Commit 4768d08d authored by Max Alekseenko's avatar Max Alekseenko

Merge remote-tracking branch 'origin/tom2drum/issue-2029' into rewards

parents ceddc76f 0defa035
NEXT_PUBLIC_SENTRY_DSN=https://sentry.io NEXT_PUBLIC_SENTRY_DSN=https://sentry.io
SENTRY_CSP_REPORT_URI=https://sentry.io SENTRY_CSP_REPORT_URI=https://sentry.io
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=xxx NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=xxx
NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY=xxx NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY=xxx
NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID=UA-XXXXXX-X NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID=UA-XXXXXX-X
NEXT_PUBLIC_MIXPANEL_PROJECT_TOKEN=xxx NEXT_PUBLIC_MIXPANEL_PROJECT_TOKEN=xxx
NEXT_PUBLIC_GROWTH_BOOK_CLIENT_KEY=xxx NEXT_PUBLIC_GROWTH_BOOK_CLIENT_KEY=xxx
......
...@@ -7,7 +7,7 @@ const RESTRICTED_MODULES = { ...@@ -7,7 +7,7 @@ const RESTRICTED_MODULES = {
{ name: 'playwright/TestApp', message: 'Please use render() fixture from test() function of playwright/lib module' }, { name: 'playwright/TestApp', message: 'Please use render() fixture from test() function of playwright/lib module' },
{ {
name: '@chakra-ui/react', name: '@chakra-ui/react',
importNames: [ 'Popover', 'Menu', 'useToast' ], importNames: [ 'Popover', 'Menu', 'PinInput', 'useToast' ],
message: 'Please use corresponding component or hook from ui/shared/chakra component instead', message: 'Please use corresponding component or hook from ui/shared/chakra component instead',
}, },
{ {
......
...@@ -30,7 +30,7 @@ jobs: ...@@ -30,7 +30,7 @@ jobs:
- name: Setup node - name: Setup node
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: 20.11.0 node-version: 20.17.0
cache: 'yarn' cache: 'yarn'
- name: Cache node_modules - name: Cache node_modules
...@@ -43,7 +43,7 @@ jobs: ...@@ -43,7 +43,7 @@ jobs:
- name: Install dependencies - name: Install dependencies
if: steps.cache-node-modules.outputs.cache-hit != 'true' if: steps.cache-node-modules.outputs.cache-hit != 'true'
run: yarn --frozen-lockfile --ignore-optional run: yarn --frozen-lockfile
- name: Run ESLint - name: Run ESLint
run: yarn lint:eslint run: yarn lint:eslint
...@@ -62,7 +62,7 @@ jobs: ...@@ -62,7 +62,7 @@ jobs:
- name: Setup node - name: Setup node
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: 20.11.0 node-version: 20.17.0
cache: 'yarn' cache: 'yarn'
- name: Cache node_modules - name: Cache node_modules
...@@ -75,10 +75,10 @@ jobs: ...@@ -75,10 +75,10 @@ jobs:
- name: Install dependencies - name: Install dependencies
if: steps.cache-node-modules.outputs.cache-hit != 'true' if: steps.cache-node-modules.outputs.cache-hit != 'true'
run: yarn --frozen-lockfile --ignore-optional run: yarn --frozen-lockfile
- name: Install script dependencies - name: Install script dependencies
run: cd ./deploy/tools/envs-validator && yarn --frozen-lockfile --ignore-optional run: cd ./deploy/tools/envs-validator && yarn --frozen-lockfile
- name: Run validation tests - name: Run validation tests
run: | run: |
...@@ -101,7 +101,7 @@ jobs: ...@@ -101,7 +101,7 @@ jobs:
- name: Setup node - name: Setup node
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: 20.11.0 node-version: 20.17.0
cache: 'yarn' cache: 'yarn'
- name: Cache node_modules - name: Cache node_modules
...@@ -114,7 +114,7 @@ jobs: ...@@ -114,7 +114,7 @@ jobs:
- name: Install dependencies - name: Install dependencies
if: steps.cache-node-modules.outputs.cache-hit != 'true' if: steps.cache-node-modules.outputs.cache-hit != 'true'
run: yarn --frozen-lockfile --ignore-optional run: yarn --frozen-lockfile
- name: Run Jest - name: Run Jest
run: yarn test:jest ${{ github.event_name == 'pull_request' && '--changedSince=origin/main' || '' }} --passWithNoTests run: yarn test:jest ${{ github.event_name == 'pull_request' && '--changedSince=origin/main' || '' }} --passWithNoTests
...@@ -133,7 +133,7 @@ jobs: ...@@ -133,7 +133,7 @@ jobs:
- name: Setup node - name: Setup node
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: 20.11.0 node-version: 20.17.0
cache: 'yarn' cache: 'yarn'
- name: Cache node_modules - name: Cache node_modules
...@@ -146,7 +146,7 @@ jobs: ...@@ -146,7 +146,7 @@ jobs:
- name: Install dependencies - name: Install dependencies
if: steps.cache-node-modules.outputs.cache-hit != 'true' if: steps.cache-node-modules.outputs.cache-hit != 'true'
run: yarn --frozen-lockfile --ignore-optional run: yarn --frozen-lockfile
- name: Install script dependencies - name: Install script dependencies
run: cd ./deploy/tools/affected-tests && yarn --frozen-lockfile run: cd ./deploy/tools/affected-tests && yarn --frozen-lockfile
...@@ -171,7 +171,7 @@ jobs: ...@@ -171,7 +171,7 @@ jobs:
(needs.pw_affected_tests.result == 'success' || needs.pw_affected_tests.result == 'skipped') (needs.pw_affected_tests.result == 'success' || needs.pw_affected_tests.result == 'skipped')
runs-on: ubuntu-latest runs-on: ubuntu-latest
container: container:
image: mcr.microsoft.com/playwright:v1.41.1-focal image: mcr.microsoft.com/playwright:v1.47.2-focal
strategy: strategy:
fail-fast: false fail-fast: false
...@@ -190,7 +190,7 @@ jobs: ...@@ -190,7 +190,7 @@ jobs:
- name: Setup node - name: Setup node
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: 20.11.0 node-version: 20.17.0
cache: 'yarn' cache: 'yarn'
- name: Cache node_modules - name: Cache node_modules
...@@ -203,7 +203,7 @@ jobs: ...@@ -203,7 +203,7 @@ jobs:
- name: Install dependencies - name: Install dependencies
if: steps.cache-node-modules.outputs.cache-hit != 'true' if: steps.cache-node-modules.outputs.cache-hit != 'true'
run: yarn --frozen-lockfile --ignore-optional run: yarn --frozen-lockfile
- name: Download affected tests list - name: Download affected tests list
if: ${{ needs.pw_affected_tests.result == 'success' }} if: ${{ needs.pw_affected_tests.result == 'success' }}
......
...@@ -21,7 +21,7 @@ jobs: ...@@ -21,7 +21,7 @@ jobs:
- name: Setup node - name: Setup node
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: 20.11.0 node-version: 20.17.0
cache: 'yarn' cache: 'yarn'
- name: Cache node_modules - name: Cache node_modules
...@@ -34,7 +34,7 @@ jobs: ...@@ -34,7 +34,7 @@ jobs:
- name: Install dependencies - name: Install dependencies
if: steps.cache-node-modules.outputs.cache-hit != 'true' if: steps.cache-node-modules.outputs.cache-hit != 'true'
run: yarn --frozen-lockfile --ignore-optional run: yarn --frozen-lockfile
- name: Make production build with source maps - name: Make production build with source maps
run: yarn build run: yarn build
......
20.11.0 20.17.0
\ No newline at end of file
# ***************************** # *****************************
# *** STAGE 1: Dependencies *** # *** STAGE 1: Dependencies ***
# ***************************** # *****************************
FROM node:20.11.0-alpine AS deps FROM node:20.17.0-alpine AS deps
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed. # Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat python3 make g++ RUN apk add --no-cache libc6-compat python3 make g++
RUN ln -sf /usr/bin/python3 /usr/bin/python RUN ln -sf /usr/bin/python3 /usr/bin/python
...@@ -31,7 +31,7 @@ RUN yarn --frozen-lockfile ...@@ -31,7 +31,7 @@ RUN yarn --frozen-lockfile
# ***************************** # *****************************
# ****** STAGE 2: Build ******* # ****** STAGE 2: Build *******
# ***************************** # *****************************
FROM node:20.11.0-alpine AS builder FROM node:20.17.0-alpine AS builder
RUN apk add --no-cache --upgrade libc6-compat bash RUN apk add --no-cache --upgrade libc6-compat bash
# pass build args to env variables # pass build args to env variables
...@@ -81,7 +81,7 @@ RUN cd ./deploy/tools/envs-validator && yarn build ...@@ -81,7 +81,7 @@ RUN cd ./deploy/tools/envs-validator && yarn build
# ******* STAGE 3: Run ******** # ******* STAGE 3: Run ********
# ***************************** # *****************************
# Production image, copy all the files and run next # Production image, copy all the files and run next
FROM node:20.11.0-alpine AS runner FROM node:20.17.0-alpine AS runner
RUN apk add --no-cache --upgrade bash curl jq unzip RUN apk add --no-cache --upgrade bash curl jq unzip
### APP ### APP
......
...@@ -14,6 +14,10 @@ ...@@ -14,6 +14,10 @@
- Updated dependency: PackageName 1 to version x.x.x. - Updated dependency: PackageName 1 to version x.x.x.
- Updated dependency: PackageName 2 to version x.x.x. - Updated dependency: PackageName 2 to version x.x.x.
## 🎨 Design updates
- New style 1.
- New style 2.
## ✨ Other Changes ## ✨ Other Changes
- Another minor change 1. - Another minor change 1.
- Another minor change 2. - Another minor change 2.
......
import type { Feature } from './types'; import type { Feature } from './types';
import stripTrailingSlash from 'lib/stripTrailingSlash'; import services from '../services';
import app from '../app';
import { getEnvValue } from '../utils'; import { getEnvValue } from '../utils';
const authUrl = stripTrailingSlash(getEnvValue('NEXT_PUBLIC_AUTH_URL') || app.baseUrl);
const logoutUrl = (() => {
try {
const envUrl = getEnvValue('NEXT_PUBLIC_LOGOUT_URL');
const auth0ClientId = getEnvValue('NEXT_PUBLIC_AUTH0_CLIENT_ID');
const returnUrl = authUrl + '/auth/logout';
if (!envUrl || !auth0ClientId) {
throw Error();
}
const url = new URL(envUrl);
url.searchParams.set('client_id', auth0ClientId);
url.searchParams.set('returnTo', returnUrl);
return url.toString();
} catch (error) {
return;
}
})();
const title = 'My account'; const title = 'My account';
const config: Feature<{ authUrl: string; logoutUrl: string }> = (() => { const config: Feature<{ isEnabled: true; recaptchaSiteKey: string }> = (() => {
if ( if (getEnvValue('NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED') === 'true' && services.reCaptchaV3.siteKey) {
getEnvValue('NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED') === 'true' &&
authUrl &&
logoutUrl
) {
return Object.freeze({ return Object.freeze({
title, title,
isEnabled: true, isEnabled: true,
authUrl, recaptchaSiteKey: services.reCaptchaV3.siteKey,
logoutUrl,
}); });
} }
......
import type { Feature } from './types';
import type { AddressProfileAPIConfig } from 'types/client/addressProfileAPIConfig';
import { getEnvValue, parseEnvJson } from '../utils';
const value = parseEnvJson<AddressProfileAPIConfig>(getEnvValue('NEXT_PUBLIC_ADDRESS_USERNAME_TAG'));
function checkApiUrlTemplate(apiUrlTemplate: string): boolean {
try {
const testUrl = apiUrlTemplate.replace('{address}', '0x0000000000000000000000000000000000000000');
new URL(testUrl).toString();
return true;
} catch (error) {
return false;
}
}
const title = 'User profile API';
const config: Feature<{
apiUrlTemplate: string;
tagLinkTemplate?: string;
tagIcon?: string;
tagBgColor?: string;
tagTextColor?: string;
}> = (() => {
if (value && checkApiUrlTemplate(value.api_url_template)) {
return Object.freeze({
title,
isEnabled: true,
apiUrlTemplate: value.api_url_template,
tagLinkTemplate: value.tag_link_template,
tagIcon: value.tag_icon,
tagBgColor: value.tag_bg_color,
tagTextColor: value.tag_text_color,
});
}
return Object.freeze({
title,
isEnabled: false,
});
})();
export default config;
...@@ -5,12 +5,12 @@ import services from '../services'; ...@@ -5,12 +5,12 @@ import services from '../services';
const title = 'Export data to CSV file'; const title = 'Export data to CSV file';
const config: Feature<{ reCaptcha: { siteKey: string }}> = (() => { const config: Feature<{ reCaptcha: { siteKey: string }}> = (() => {
if (services.reCaptcha.siteKey) { if (services.reCaptchaV3.siteKey) {
return Object.freeze({ return Object.freeze({
title, title,
isEnabled: true, isEnabled: true,
reCaptcha: { reCaptcha: {
siteKey: services.reCaptcha.siteKey, siteKey: services.reCaptchaV3.siteKey,
}, },
}); });
} }
......
...@@ -33,6 +33,7 @@ export { default as stats } from './stats'; ...@@ -33,6 +33,7 @@ export { default as stats } from './stats';
export { default as suave } from './suave'; export { default as suave } from './suave';
export { default as txInterpretation } from './txInterpretation'; export { default as txInterpretation } from './txInterpretation';
export { default as userOps } from './userOps'; export { default as userOps } from './userOps';
export { default as addressProfileAPI } from './addressProfileAPI';
export { default as validators } from './validators'; export { default as validators } from './validators';
export { default as verifiedTokens } from './verifiedTokens'; export { default as verifiedTokens } from './verifiedTokens';
export { default as web3Wallet } from './web3Wallet'; export { default as web3Wallet } from './web3Wallet';
...@@ -9,7 +9,7 @@ const apiHost = getEnvValue('NEXT_PUBLIC_ADMIN_SERVICE_API_HOST'); ...@@ -9,7 +9,7 @@ const apiHost = getEnvValue('NEXT_PUBLIC_ADMIN_SERVICE_API_HOST');
const title = 'Public tag submission'; const title = 'Public tag submission';
const config: Feature<{ api: { endpoint: string; basePath: string } }> = (() => { const config: Feature<{ api: { endpoint: string; basePath: string } }> = (() => {
if (services.reCaptcha.siteKey && addressMetadata.isEnabled && apiHost) { if (services.reCaptchaV3.siteKey && addressMetadata.isEnabled && apiHost) {
return Object.freeze({ return Object.freeze({
title, title,
isEnabled: true, isEnabled: true,
......
import { getEnvValue } from './utils'; import { getEnvValue } from './utils';
export default Object.freeze({ export default Object.freeze({
reCaptcha: { reCaptchaV3: {
siteKey: getEnvValue('NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY'), siteKey: getEnvValue('NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY'),
}, },
}); });
# Set of ENVs for BXN Testnet network explorer
# https://blackfort-testnet.blockscout.com
# This is an auto-generated file. To update all values, run "yarn dev:preset:sync --name=blackfort_testnet"
# Local ENVs
NEXT_PUBLIC_APP_PROTOCOL=http
NEXT_PUBLIC_APP_HOST=localhost
NEXT_PUBLIC_APP_PORT=3000
NEXT_PUBLIC_APP_ENV=development
NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL=ws
# Instance ENVs
NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://admin-rs.services.blockscout.com
NEXT_PUBLIC_API_BASE_PATH=/
NEXT_PUBLIC_API_HOST=blackfort-testnet.blockscout.com
NEXT_PUBLIC_API_SPEC_URL=https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml
NEXT_PUBLIC_CONTRACT_CODE_IDES=[{'title':'Remix IDE','url':'https://remix.ethereum.org/?address={hash}&blockscout={domain}','icon_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/ide-icons/remix.png'}]
NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info.services.blockscout.com
NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/blackfort-testnet.json
NEXT_PUBLIC_FOOTER_LINKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/footer-links/blackfort.json
NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0xcb4140e22cde3412eb5aecdedf2403032c7a251f5c96b11122aca5b1b88ed953
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs']
NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND=linear-gradient(92deg, rgb(3, 150, 254) 0.24%, rgb(36, 209, 245) 98.31%)
NEXT_PUBLIC_IS_TESTNET=true
NEXT_PUBLIC_MARKETPLACE_ENABLED=false
NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com
NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18
NEXT_PUBLIC_NETWORK_CURRENCY_NAME=TBXN
NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=TBXN
NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/blackfort.svg
NEXT_PUBLIC_NETWORK_ICON_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/blackfort-dark.svg
NEXT_PUBLIC_NETWORK_ID=4777
NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/blackfort.svg
NEXT_PUBLIC_NETWORK_LOGO_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/blackfort-dark.svg
NEXT_PUBLIC_NETWORK_NAME=BXN Testnet
NEXT_PUBLIC_NETWORK_RPC_URL=https://testnet.blackfort.network/rpc
NEXT_PUBLIC_NETWORK_SHORT_NAME=BXN Testnet
NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED=true
NEXT_PUBLIC_OG_IMAGE_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/og-images/blackfort.png
NEXT_PUBLIC_STATS_API_HOST=https://stats-blackfort-testnet.k8s.blockscout.com
NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout
NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=true
NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
NEXT_PUBLIC_VALIDATORS_CHAIN_TYPE=blackfort
\ No newline at end of file
...@@ -14,7 +14,7 @@ NEXT_PUBLIC_AD_ADBUTLER_CONFIG_DESKTOP={ "id": "632019", "width": "728", "height ...@@ -14,7 +14,7 @@ NEXT_PUBLIC_AD_ADBUTLER_CONFIG_DESKTOP={ "id": "632019", "width": "728", "height
NEXT_PUBLIC_AD_ADBUTLER_CONFIG_MOBILE={ "id": "632018", "width": "320", "height": "100" } NEXT_PUBLIC_AD_ADBUTLER_CONFIG_MOBILE={ "id": "632018", "width": "320", "height": "100" }
NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://admin-rs.services.blockscout.com NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://admin-rs.services.blockscout.com
NEXT_PUBLIC_API_BASE_PATH=/ NEXT_PUBLIC_API_BASE_PATH=/
NEXT_PUBLIC_API_HOST=eth-sepolia.blockscout.com NEXT_PUBLIC_API_HOST=eth-sepolia.k8s-dev.blockscout.com
NEXT_PUBLIC_API_SPEC_URL=https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml NEXT_PUBLIC_API_SPEC_URL=https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml
NEXT_PUBLIC_CONTRACT_CODE_IDES=[{'title':'Remix IDE','url':'https://remix.ethereum.org/?address={hash}&blockscout={domain}','icon_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/ide-icons/remix.png'}] NEXT_PUBLIC_CONTRACT_CODE_IDES=[{'title':'Remix IDE','url':'https://remix.ethereum.org/?address={hash}&blockscout={domain}','icon_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/ide-icons/remix.png'}]
NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info.services.blockscout.com NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info.services.blockscout.com
...@@ -59,7 +59,7 @@ NEXT_PUBLIC_OG_IMAGE_URL=https://raw.githubusercontent.com/blockscout/frontend-c ...@@ -59,7 +59,7 @@ NEXT_PUBLIC_OG_IMAGE_URL=https://raw.githubusercontent.com/blockscout/frontend-c
NEXT_PUBLIC_OTHER_LINKS=[{'url':'https://sepolia.drpc.org?ref=559183','text':'Public RPC'}] NEXT_PUBLIC_OTHER_LINKS=[{'url':'https://sepolia.drpc.org?ref=559183','text':'Public RPC'}]
NEXT_PUBLIC_SAFE_TX_SERVICE_URL=https://safe-transaction-sepolia.safe.global NEXT_PUBLIC_SAFE_TX_SERVICE_URL=https://safe-transaction-sepolia.safe.global
NEXT_PUBLIC_SENTRY_ENABLE_TRACING=true NEXT_PUBLIC_SENTRY_ENABLE_TRACING=true
NEXT_PUBLIC_STATS_API_HOST=https://stats-sepolia.k8s.blockscout.com NEXT_PUBLIC_STATS_API_HOST=https://stats-sepolia.k8s-dev.blockscout.com
NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=noves NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=noves
NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=true NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=true
NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
......
...@@ -49,4 +49,4 @@ NEXT_PUBLIC_AUTH0_CLIENT_ID=xxx ...@@ -49,4 +49,4 @@ NEXT_PUBLIC_AUTH0_CLIENT_ID=xxx
NEXT_PUBLIC_STATS_API_HOST=https://localhost:3004 NEXT_PUBLIC_STATS_API_HOST=https://localhost:3004
NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://localhost:3005 NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://localhost:3005
NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://localhost:3006 NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://localhost:3006
NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY=xxx NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY=xxx
...@@ -52,5 +52,5 @@ NEXT_PUBLIC_CONTRACT_INFO_API_HOST=http://localhost:3005 ...@@ -52,5 +52,5 @@ NEXT_PUBLIC_CONTRACT_INFO_API_HOST=http://localhost:3005
NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=http://localhost:3006 NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=http://localhost:3006
NEXT_PUBLIC_METADATA_SERVICE_API_HOST=http://localhost:3007 NEXT_PUBLIC_METADATA_SERVICE_API_HOST=http://localhost:3007
NEXT_PUBLIC_NAME_SERVICE_API_HOST=http://localhost:3008 NEXT_PUBLIC_NAME_SERVICE_API_HOST=http://localhost:3008
NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY=xxx NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY=xxx
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=xxx NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=xxx
# Set of ENVs for Zora Mainnet network explorer
# https://explorer.zora.energy
# This is an auto-generated file. To update all values, run "yarn dev:preset:sync --name=zora"
# Local ENVs
NEXT_PUBLIC_APP_PROTOCOL=http
NEXT_PUBLIC_APP_HOST=localhost
NEXT_PUBLIC_APP_PORT=3000
NEXT_PUBLIC_APP_ENV=development
NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL=ws
# Instance ENVs
NEXT_PUBLIC_AD_BANNER_PROVIDER=none
NEXT_PUBLIC_AD_TEXT_PROVIDER=none
NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://admin-rs.services.blockscout.com
NEXT_PUBLIC_API_BASE_PATH=/
NEXT_PUBLIC_API_HOST=explorer.zora.energy
NEXT_PUBLIC_API_SPEC_URL=https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml
NEXT_PUBLIC_CONTRACT_CODE_IDES=[{'title':'Remix IDE','url':'https://remix.ethereum.org/?address={hash}&blockscout={domain}','icon_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/ide-icons/remix.png'}]
NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info.services.blockscout.com
NEXT_PUBLIC_DEFI_DROPDOWN_ITEMS=[{'text':'Swap','icon':'swap','dappId':'uniswap'}]
NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/zora.json
NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0x6d54c0226a57f5bc854f8aa589bb15113388f984f318c9e1b2722115e4e35873
NEXT_PUBLIC_HAS_CONTRACT_AUDIT_REPORTS=true
NEXT_PUBLIC_HAS_USER_OPS=true
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs']
NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND=linear-gradient(89deg, rgb(63, 36, 22) 0.56%, rgb(44, 56, 105) 98.31%)
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
NEXT_PUBLIC_LOGOUT_URL=https://zora-blockscout.us.auth0.com/v2/logout
NEXT_PUBLIC_MARKETPLACE_ENABLED=true
NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_API_KEY=patbqG4V2CI998jAq.9810c58c9de973ba2650621c94559088cbdfa1a914498e385621ed035d33c0d0
NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_BASE_ID=appGkvtmKI7fXE4Vs
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/appiy5yijZpMMSKjT/shr6uMGPKjj1DK7NL
NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM=https://airtable.com/appiy5yijZpMMSKjT/pag3t82DUCyhGRZZO/form
NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com
NEXT_PUBLIC_NAME_SERVICE_API_HOST=https://bens.services.blockscout.com
NEXT_PUBLIC_NAVIGATION_HIGHLIGHTED_ROUTES=['/apps']
NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18
NEXT_PUBLIC_NETWORK_CURRENCY_NAME=Ether
NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ETH
NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'GeckoTerminal','logo':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/explorer-logos/geckoterminal.png','baseUrl':'https://www.geckoterminal.com/','paths':{'token':'/zora-network/pools'}}]
NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/zora.svg
NEXT_PUBLIC_NETWORK_ICON_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/zora-dark.svg
NEXT_PUBLIC_NETWORK_ID=7777777
NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/zora.svg
NEXT_PUBLIC_NETWORK_LOGO_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/zora-dark.svg
NEXT_PUBLIC_NETWORK_NAME=Zora Mainnet
NEXT_PUBLIC_NETWORK_RPC_URL=https://rpc.zora.energy
NEXT_PUBLIC_NETWORK_SHORT_NAME=Zora Mainnet
NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE=validation
NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED=true
NEXT_PUBLIC_OG_IMAGE_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/og-images/zora-mainnet.png
NEXT_PUBLIC_ROLLUP_L1_BASE_URL=https://eth.blockscout.com/
NEXT_PUBLIC_ROLLUP_L2_WITHDRAWAL_URL=https://bridge.zora.energy
NEXT_PUBLIC_ROLLUP_TYPE=optimistic
NEXT_PUBLIC_STATS_API_HOST=https://stats-l2-zora-mainnet.k8s-prod-1.blockscout.com
NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout
NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
NEXT_PUBLIC_MULTICHAIN_BALANCE_PROVIDER_CONFIG={'name': 'zerion', 'dapp_id': 'zerion', 'url_template': 'https://app.zerion.io/{address}/overview?utm_source=blockscout&utm_medium=address', 'logo': 'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-logos/zerion.svg'}
NEXT_PUBLIC_ADDRESS_USERNAME_TAG={'api_url_template': 'https://api.zora.co/discover/user/{address}', 'tag_link_template': 'httpszora.co/{username}', 'tag_icon': 'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/zora.svg', 'tag_bg_color': 'rgba(0,0,0)', 'tag_text_color': 'rgba(255,255,255)'}
\ No newline at end of file
...@@ -147,4 +147,15 @@ function printDeprecationWarning(envsMap: Record<string, string>) { ...@@ -147,4 +147,15 @@ function printDeprecationWarning(envsMap: Record<string, string>) {
console.warn('The NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR and NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND variables are now deprecated and will be removed in the next release. Please migrate to the NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG variable.'); console.warn('The NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR and NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND variables are now deprecated and will be removed in the next release. Please migrate to the NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG variable.');
console.log('❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗\n'); console.log('❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗\n');
} }
if (
envsMap.NEXT_PUBLIC_AUTH0_CLIENT_ID ||
envsMap.NEXT_PUBLIC_AUTH_URL ||
envsMap.NEXT_PUBLIC_LOGOUT_URL
) {
console.log('❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗');
// eslint-disable-next-line max-len
console.warn('The NEXT_PUBLIC_AUTH0_CLIENT_ID, NEXT_PUBLIC_AUTH_URL and NEXT_PUBLIC_LOGOUT_URL variables are now deprecated and will be removed in the next release.');
console.log('❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗\n');
}
} }
...@@ -10,6 +10,7 @@ declare module 'yup' { ...@@ -10,6 +10,7 @@ declare module 'yup' {
import * as yup from 'yup'; import * as yup from 'yup';
import type { AdButlerConfig } from '../../../types/client/adButlerConfig'; import type { AdButlerConfig } from '../../../types/client/adButlerConfig';
import type { AddressProfileAPIConfig } from '../../../types/client/addressProfileAPIConfig';
import { SUPPORTED_AD_TEXT_PROVIDERS, SUPPORTED_AD_BANNER_PROVIDERS, SUPPORTED_AD_BANNER_ADDITIONAL_PROVIDERS } from '../../../types/client/adProviders'; import { SUPPORTED_AD_TEXT_PROVIDERS, SUPPORTED_AD_BANNER_PROVIDERS, SUPPORTED_AD_BANNER_ADDITIONAL_PROVIDERS } from '../../../types/client/adProviders';
import type { AdTextProviders, AdBannerProviders, AdBannerAdditionalProviders } from '../../../types/client/adProviders'; import type { AdTextProviders, AdBannerProviders, AdBannerAdditionalProviders } from '../../../types/client/adProviders';
import { SMART_CONTRACT_EXTRA_VERIFICATION_METHODS, type ContractCodeIde, type SmartContractVerificationMethodExtra } from '../../../types/client/contract'; import { SMART_CONTRACT_EXTRA_VERIFICATION_METHODS, type ContractCodeIde, type SmartContractVerificationMethodExtra } from '../../../types/client/contract';
...@@ -803,10 +804,24 @@ const schema = yup ...@@ -803,10 +804,24 @@ const schema = yup
), ),
}), }),
NEXT_PUBLIC_SAVE_ON_GAS_ENABLED: yup.boolean(), NEXT_PUBLIC_SAVE_ON_GAS_ENABLED: yup.boolean(),
NEXT_PUBLIC_ADDRESS_USERNAME_TAG: yup
.mixed()
.test('shape', 'Invalid schema were provided for NEXT_PUBLIC_ADDRESS_USERNAME_TAG, it should have api_url_template', (data) => {
const isUndefined = data === undefined;
const valueSchema = yup.object<AddressProfileAPIConfig>().transform(replaceQuotes).json().shape({
api_url_template: yup.string().required(),
tag_link_template: yup.string(),
tag_icon: yup.string(),
tag_bg_color: yup.string(),
tag_text_color: yup.string(),
});
return isUndefined || valueSchema.isValidSync(data);
}),
// 6. External services envs // 6. External services envs
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID: yup.string(), NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID: yup.string(),
NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY: yup.string(), NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY: yup.string(),
NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID: yup.string(), NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID: yup.string(),
NEXT_PUBLIC_MIXPANEL_PROJECT_TOKEN: yup.string(), NEXT_PUBLIC_MIXPANEL_PROJECT_TOKEN: yup.string(),
NEXT_PUBLIC_GROWTH_BOOK_CLIENT_KEY: yup.string(), NEXT_PUBLIC_GROWTH_BOOK_CLIENT_KEY: yup.string(),
......
...@@ -3,7 +3,7 @@ NEXT_PUBLIC_AUTH_URL=https://example.com ...@@ -3,7 +3,7 @@ NEXT_PUBLIC_AUTH_URL=https://example.com
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
NEXT_PUBLIC_LOGOUT_URL=https://example.com NEXT_PUBLIC_LOGOUT_URL=https://example.com
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=xxx NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=xxx
NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY=xxx NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY=xxx
NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID=UA-XXXXXX-X NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID=UA-XXXXXX-X
NEXT_PUBLIC_MIXPANEL_PROJECT_TOKEN=xxx NEXT_PUBLIC_MIXPANEL_PROJECT_TOKEN=xxx
NEXT_PUBLIC_GROWTH_BOOK_CLIENT_KEY=xxx NEXT_PUBLIC_GROWTH_BOOK_CLIENT_KEY=xxx
......
...@@ -77,7 +77,7 @@ frontend: ...@@ -77,7 +77,7 @@ frontend:
SENTRY_CSP_REPORT_URI: ref+vault://deployment-values/blockscout/dev/review-l2?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/SENTRY_CSP_REPORT_URI SENTRY_CSP_REPORT_URI: ref+vault://deployment-values/blockscout/dev/review-l2?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/SENTRY_CSP_REPORT_URI
NEXT_PUBLIC_AUTH0_CLIENT_ID: ref+vault://deployment-values/blockscout/dev/review-l2?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_AUTH0_CLIENT_ID NEXT_PUBLIC_AUTH0_CLIENT_ID: ref+vault://deployment-values/blockscout/dev/review-l2?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_AUTH0_CLIENT_ID
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID: ref+vault://deployment-values/blockscout/dev/review-l2?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID: ref+vault://deployment-values/blockscout/dev/review-l2?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID
NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY: ref+vault://deployment-values/blockscout/dev/review-l2?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY
NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID: ref+vault://deployment-values/blockscout/dev/review-l2?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID: ref+vault://deployment-values/blockscout/dev/review-l2?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID
FAVICON_GENERATOR_API_KEY: ref+vault://deployment-values/blockscout/common?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_FAVICON_GENERATOR_API_KEY FAVICON_GENERATOR_API_KEY: ref+vault://deployment-values/blockscout/common?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_FAVICON_GENERATOR_API_KEY
NEXT_PUBLIC_OG_IMAGE_URL: https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/og-images/base-mainnet.png NEXT_PUBLIC_OG_IMAGE_URL: https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/og-images/base-mainnet.png
NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY: ref+vault://deployment-values/blockscout/common?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY
...@@ -83,8 +83,8 @@ frontend: ...@@ -83,8 +83,8 @@ frontend:
SENTRY_CSP_REPORT_URI: ref+vault://deployment-values/blockscout/dev/review?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/SENTRY_CSP_REPORT_URI SENTRY_CSP_REPORT_URI: ref+vault://deployment-values/blockscout/dev/review?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/SENTRY_CSP_REPORT_URI
NEXT_PUBLIC_AUTH0_CLIENT_ID: ref+vault://deployment-values/blockscout/dev/review?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_AUTH0_CLIENT_ID NEXT_PUBLIC_AUTH0_CLIENT_ID: ref+vault://deployment-values/blockscout/dev/review?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_AUTH0_CLIENT_ID
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID: ref+vault://deployment-values/blockscout/dev/review?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID: ref+vault://deployment-values/blockscout/dev/review?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID
NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY: ref+vault://deployment-values/blockscout/dev/review?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY
NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID: ref+vault://deployment-values/blockscout/dev/review?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID: ref+vault://deployment-values/blockscout/dev/review?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID
FAVICON_GENERATOR_API_KEY: ref+vault://deployment-values/blockscout/common?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_FAVICON_GENERATOR_API_KEY FAVICON_GENERATOR_API_KEY: ref+vault://deployment-values/blockscout/common?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_FAVICON_GENERATOR_API_KEY
NEXT_PUBLIC_GROWTH_BOOK_CLIENT_KEY: ref+vault://deployment-values/blockscout/dev/review?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_GROWTH_BOOK_CLIENT_KEY NEXT_PUBLIC_GROWTH_BOOK_CLIENT_KEY: ref+vault://deployment-values/blockscout/dev/review?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_GROWTH_BOOK_CLIENT_KEY
NEXT_PUBLIC_MIXPANEL_PROJECT_TOKEN: ref+vault://deployment-values/blockscout/common?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_MIXPANEL_PROJECT_TOKEN NEXT_PUBLIC_MIXPANEL_PROJECT_TOKEN: ref+vault://deployment-values/blockscout/common?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_MIXPANEL_PROJECT_TOKEN
NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY: ref+vault://deployment-values/blockscout/common?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY
...@@ -10,4 +10,5 @@ ...@@ -10,4 +10,5 @@
| NEXT_PUBLIC_HOMEPAGE_SHOW_GAS_TRACKER | `boolean` | Set to false if network doesn't have gas tracker | - | `true` | `false` | - | v1.25.0 | Replaced by NEXT_PUBLIC_GAS_TRACKER_ENABLED | | NEXT_PUBLIC_HOMEPAGE_SHOW_GAS_TRACKER | `boolean` | Set to false if network doesn't have gas tracker | - | `true` | `false` | - | v1.25.0 | Replaced by NEXT_PUBLIC_GAS_TRACKER_ENABLED |
| NEXT_PUBLIC_NETWORK_GOVERNANCE_TOKEN_SYMBOL | `string` | Network governance token symbol | - | - | `GNO` | v1.12.0 | v1.29.0 | Replaced by NEXT_PUBLIC_NETWORK_SECONDARY_COIN_SYMBOL | | NEXT_PUBLIC_NETWORK_GOVERNANCE_TOKEN_SYMBOL | `string` | Network governance token symbol | - | - | `GNO` | v1.12.0 | v1.29.0 | Replaced by NEXT_PUBLIC_NETWORK_SECONDARY_COIN_SYMBOL |
| NEXT_PUBLIC_SWAP_BUTTON_URL | `string` | Application ID in the marketplace or website URL | - | - | `uniswap` | v1.24.0 | v1.31.0 | Replaced by NEXT_PUBLIC_DEFI_DROPDOWN_ITEMS | | NEXT_PUBLIC_SWAP_BUTTON_URL | `string` | Application ID in the marketplace or website URL | - | - | `uniswap` | v1.24.0 | v1.31.0 | Replaced by NEXT_PUBLIC_DEFI_DROPDOWN_ITEMS |
| NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME | `boolean` | Set to false if average block time is useless for the network | - | `true` | `false` | v1.0.x+ | v1.35.0 | Replaces by NEXT_PUBLIC_HOMEPAGE_STATS | NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME | `boolean` | Set to false if average block time is useless for the network | - | `true` | `false` | v1.0.x+ | v1.35.0 | Replaced by NEXT_PUBLIC_HOMEPAGE_STATS |
\ No newline at end of file | NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY | `string` | Google reCAPTCHA v2 site key | - | - | `<your-secret>` | v1.36.0 | Replaced by NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY |
...@@ -54,6 +54,7 @@ Please be aware that all environment variables prefixed with `NEXT_PUBLIC_` will ...@@ -54,6 +54,7 @@ Please be aware that all environment variables prefixed with `NEXT_PUBLIC_` will
- [Data availability](ENVS.md#data-availability) - [Data availability](ENVS.md#data-availability)
- [Bridged tokens](ENVS.md#bridged-tokens) - [Bridged tokens](ENVS.md#bridged-tokens)
- [Safe{Core} address tags](ENVS.md#safecore-address-tags) - [Safe{Core} address tags](ENVS.md#safecore-address-tags)
- [Address profile API](ENVS.md#address-profile-api)
- [SUAVE chain](ENVS.md#suave-chain) - [SUAVE chain](ENVS.md#suave-chain)
- [MetaSuites extension](ENVS.md#metasuites-extension) - [MetaSuites extension](ENVS.md#metasuites-extension)
- [Validators list](ENVS.md#validators-list) - [Validators list](ENVS.md#validators-list)
...@@ -341,9 +342,10 @@ Settings for meta tags, OG tags and SEO ...@@ -341,9 +342,10 @@ Settings for meta tags, OG tags and SEO
| Variable | Type| Description | Compulsoriness | Default value | Example value | Version | | Variable | Type| Description | Compulsoriness | Default value | Example value | Version |
| --- | --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- | --- |
| NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED | `boolean` | Set to true if network has account feature | Required | - | `true` | v1.0.x+ | | NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED | `boolean` | Set to true if network has account feature | Required | - | `true` | v1.0.x+ |
| NEXT_PUBLIC_AUTH0_CLIENT_ID | `string` | Client id for [Auth0](https://auth0.com/) provider | Required | - | `<your-secret>` | v1.0.x+ | | NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY | `boolean` | See [below](ENVS.md#google-recaptcha) | Required | - | `<your-secret>` | v1.36.0+ |
| NEXT_PUBLIC_AUTH_URL | `string` | Account auth base url; it is used for building login URL (`${ NEXT_PUBLIC_AUTH_URL }/auth/auth0`) and logout return URL (`${ NEXT_PUBLIC_AUTH_URL }/auth/logout`); if not provided the base app URL will be used instead | Required | - | `https://blockscout.com` | v1.0.x+ | | NEXT_PUBLIC_AUTH0_CLIENT_ID | `string` | **DEPRECATED** Client id for [Auth0](https://auth0.com/) provider | - | - | `<your-secret>` | v1.0.x+ |
| NEXT_PUBLIC_LOGOUT_URL | `string` | Account logout url. Required if account is supported for the app instance. | Required | - | `https://blockscoutcom.us.auth0.com/v2/logout` | v1.0.x+ | | NEXT_PUBLIC_AUTH_URL | `string` | **DEPRECATED** Account auth base url; it is used for building login URL (`${ NEXT_PUBLIC_AUTH_URL }/auth/auth0`) and logout return URL (`${ NEXT_PUBLIC_AUTH_URL }/auth/logout`); if not provided the base app URL will be used instead | - | - | `https://blockscout.com` | v1.0.x+ |
| NEXT_PUBLIC_LOGOUT_URL | `string` | **DEPRECATED** Account logout url. Required if account is supported for the app instance. | - | - | `https://blockscoutcom.us.auth0.com/v2/logout` | v1.0.x+ |
&nbsp; &nbsp;
...@@ -440,7 +442,7 @@ This feature is **enabled by default** with the `coinzilla` ads provider. To swi ...@@ -440,7 +442,7 @@ This feature is **enabled by default** with the `coinzilla` ads provider. To swi
| Variable | Type| Description | Compulsoriness | Default value | Example value | Version | | Variable | Type| Description | Compulsoriness | Default value | Example value | Version |
| --- | --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- | --- |
| NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY | `string` | See [below](ENVS.md#google-recaptcha) | true | - | `<your-secret>` | v1.0.x+ | | NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY | `string` | See [below](ENVS.md#google-recaptcha) | true | - | `<your-secret>` | v1.36.0+ |
&nbsp; &nbsp;
...@@ -654,6 +656,28 @@ For the smart contract addresses which are [Safe{Core} accounts](https://safe.gl ...@@ -654,6 +656,28 @@ For the smart contract addresses which are [Safe{Core} accounts](https://safe.gl
&nbsp; &nbsp;
### Address profile API
This feature allows the integration of an external API to fetch user info for addresses or contracts. When configured, if the API returns a username, a public tag with a custom link will be displayed in the address page header.
| Variable | Type| Description | Compulsoriness | Default value | Example value | Version |
| --- | --- | --- | --- | --- | --- | --- |
| NEXT_PUBLIC_ADDRESS_USERNAME_TAG | `{api_url: string; tag_link_template: string; tag_icon: string; tag_bg_color: string; tag_text_color: string}` | Address profile API tag configuration properties. See [below](#user-profile-api-configuration-properties). | - | - | `uniswap` | v1.35.0+ |
&nbsp;
#### Address profile API configuration properties
| Variable | Type| Description | Compulsoriness | Default value | Example value |
| --- | --- | --- | --- | --- | --- |
| api_url_template | `string` | User profile API URL. Should be a template with `{address}` variable | Required | - | `https://example-api.com/{address}` |
| tag_link_template | `string` | External link to the profile. Should be a template with `{username}` variable | - | - | `https://example.com/{address}` |
| tag_icon | `string` | Public tag icon (.svg) url | - | - | `https://example.com/icon.svg` |
| tag_bg_color | `string` | Public tag background color (escape "#" symbol if you use HEX color codes or use rgba-value instead) | - | - | `\#000000` |
| tag_text_color | `string` | Public tag text color (escape "#" symbol if you use HEX color codes or use rgba-value instead) | - | - | `\#FFFFFF` |
&nbsp;
### SUAVE chain ### SUAVE chain
For blockchains that implement SUAVE architecture additional fields will be shown on the transaction page ("Allowed peekers", "Kettle"). Users also will be able to see the list of all transactions for a particular Kettle in the separate view. For blockchains that implement SUAVE architecture additional fields will be shown on the transaction page ("Allowed peekers", "Kettle"). Users also will be able to see the list of all transactions for a particular Kettle in the separate view.
...@@ -680,7 +704,7 @@ The feature enables the Validators page which provides detailed information abou ...@@ -680,7 +704,7 @@ The feature enables the Validators page which provides detailed information abou
| Variable | Type| Description | Compulsoriness | Default value | Example value | Version | | Variable | Type| Description | Compulsoriness | Default value | Example value | Version |
| --- | --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- | --- |
| NEXT_PUBLIC_VALIDATORS_CHAIN_TYPE | `'stability'` | Chain type | Required | - | `'stability'` | v1.25.0+ | | NEXT_PUBLIC_VALIDATORS_CHAIN_TYPE | `'stability' \| 'blackfort'` | Chain type | Required | - | `'stability'` | v1.25.0+ |
&nbsp; &nbsp;
...@@ -784,4 +808,4 @@ For obtaining the variables values please refer to [reCAPTCHA documentation](htt ...@@ -784,4 +808,4 @@ For obtaining the variables values please refer to [reCAPTCHA documentation](htt
| Variable | Type| Description | Compulsoriness | Default value | Example value | Version | | Variable | Type| Description | Compulsoriness | Default value | Example value | Version |
| --- | --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- | --- |
| NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY | `string` | Site key | - | - | `<your-secret>` | v1.0.x+ | | NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY | `string` | Google reCAPTCHA v3 site key | - | - | `<your-secret>` | v1.36.0+ |
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.3 3.7a1.7 1.7 0 0 1 3.4 0v.903a1 1 0 1 0 2 0V3.7a3.7 3.7 0 0 0-7.4 0v6.272a1 1 0 0 0 2 0V3.7Zm5.4 6.302a1 1 0 0 0-2 0V16.3a1.7 1.7 0 0 1-3.4 0v-.914a1 1 0 1 0-2 0v.914a3.7 3.7 0 1 0 7.4 0v-6.298ZM3.692 8.3C2.76 8.3 2 9.059 2 10c0 .94.76 1.7 1.693 1.7H10a1 1 0 1 1 0 2H3.693A3.696 3.696 0 0 1 0 10c0-2.04 1.65-3.7 3.693-3.7h.902a1 1 0 0 1 0 2h-.902ZM10 6.3a1 1 0 0 0 0 2h6.294C17.238 8.3 18 9.064 18 10c0 .937-.761 1.7-1.705 1.7h-.865a1 1 0 1 0 0 2h.865A3.702 3.702 0 0 0 20 10c0-2.046-1.66-3.7-3.705-3.7H10Z" fill="currentColor"/>
</svg>
<svg viewBox="0 0 177 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M166.961 7.644a2 2 0 0 0-2.251-2.812l-76.313 17.5a2 2 0 0 0-.727 3.569l17.58 12.744v17.636a2.001 2.001 0 0 0 3.321 1.502l10.716-9.426 21.384 9.744a2 2 0 0 0 2.634-.957l23.656-49.5ZM108.308 35.92 93.583 25.247l62.923-14.43-48.198 25.104Zm.942 1.764v14.173l8.367-7.36a.385.385 0 0 1 .022-.018l.016-.014a.998.998 0 0 1 .214-.242l37.608-30.616-46.227 24.077Zm31.293 15.962-19.927-9.08 39.719-32.335-19.792 41.415ZM93.278 65.729a1.5 1.5 0 0 0 1.93 2.296 93.435 93.435 0 0 0 2.449-2.13 57.65 57.65 0 0 0 .819-.753l.044-.042.012-.011.004-.004.001-.001L97.5 64l1.038 1.083a1.5 1.5 0 0 0-2.075-2.167l-.002.002-.008.008-.037.035a19.011 19.011 0 0 1-.154.145 90.663 90.663 0 0 1-2.984 2.623Zm-5.037 7.714a1.5 1.5 0 0 0-1.751-2.436 105.47 105.47 0 0 1-7.163 4.73 1.5 1.5 0 1 0 1.547 2.57c2.69-1.618 5.17-3.284 7.367-4.864Zm-15.172 9.06a1.5 1.5 0 0 0-1.28-2.714c-2.556 1.205-5.207 2.277-7.906 3.13a1.5 1.5 0 0 0 .905 2.86c2.847-.9 5.624-2.024 8.28-3.276Zm-17.046 5.22a1.5 1.5 0 0 0-.358-2.98A34.938 34.938 0 0 1 51.5 85c-1.392 0-2.728-.09-4.012-.26a1.5 1.5 0 1 0-.394 2.973A33.44 33.44 0 0 0 51.5 88c1.51 0 3.02-.097 4.523-.278Zm-17.422-2.388a1.5 1.5 0 1 0 1.212-2.745c-2.517-1.11-4.783-2.55-6.816-4.194a1.5 1.5 0 1 0-1.887 2.332c2.216 1.792 4.705 3.377 7.491 4.607ZM24.974 74.523a1.5 1.5 0 1 0 2.338-1.881C25.51 70.403 24 68.076 22.756 65.86a1.5 1.5 0 0 0-2.616 1.47c1.311 2.333 2.911 4.803 4.834 7.193Zm-8.515-15a1.5 1.5 0 0 0 2.796-1.086 49.986 49.986 0 0 1-1-2.813 32.043 32.043 0 0 1-.29-.956l-.013-.045-.002-.01a1.5 1.5 0 0 0-2.899.775l1.45-.388-1.45.388.001.003.002.005.004.018.018.061.064.227c.057.196.143.478.258.837.23.718.579 1.741 1.06 2.984Z" fill="currentColor"/>
</svg>
<svg viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.605 21a2.26 2.26 0 0 1-1.614-.665l-6.314-6.318a2.266 2.266 0 0 1 0-3.23l8.45-8.457C10.888 1.57 12.265 1 13.31 1h5.412C19.956 1 21 2.045 21 3.28v5.416c0 .403-.085.856-.233 1.304H19.18c.208-.443.348-.944.348-1.352V3.233a.851.851 0 0 0-.854-.855h-5.365v.047c-.665 0-1.71.428-2.184.903l-8.451 8.456a.832.832 0 0 0 0 1.188l6.314 6.318c.332.332.902.332 1.187 0l1.818-1.82v2.09l-.773.775A2.26 2.26 0 0 1 9.605 21Z" fill="currentColor"/>
<path d="m7.991 20.335-.177.177.177-.177Zm-6.314-6.318.176-.177-.176.177Zm0-3.23.176.176-.176-.177Zm8.45-8.457-.176-.177.177.177ZM20.768 10v.25h.181l.057-.172-.238-.078Zm-1.587 0-.226-.106-.168.356h.394V10ZM13.31 2.378v-.25h-.25v.25h.25Zm0 .047v.25h.25v-.25h-.25Zm-2.184.903-.177-.177.177.177Zm-8.451 8.456.176.177-.176-.177Zm0 1.188-.177.176.177-.176Zm6.314 6.318-.177.177.177-.177Zm1.187 0-.177-.177-.007.007-.006.007.19.163Zm1.818-1.82h.25v-.604l-.426.428.176.176Zm0 2.09.177.177.073-.073v-.103h-.25Zm-.773.775.176.177-.176-.177Zm-3.406.177a2.51 2.51 0 0 0 1.791.738v-.5a2.01 2.01 0 0 1-1.437-.592l-.354.354ZM1.5 14.193l6.314 6.319.354-.354-6.315-6.318-.353.353Zm0-3.583c-1 1-1 2.583 0 3.583l.353-.353a2.016 2.016 0 0 1 0-2.877L1.5 10.61Zm8.45-8.457L1.5 10.61l.353.353 8.451-8.456-.353-.354ZM13.31.75c-.564 0-1.202.153-1.794.4-.592.246-1.156.595-1.564 1.003l.353.354c.352-.352.856-.668 1.403-.896.548-.229 1.12-.361 1.602-.361v-.5Zm5.412 0H13.31v.5h5.412v-.5Zm2.529 2.53c0-1.373-1.156-2.53-2.529-2.53v.5c1.096 0 2.029.933 2.029 2.03h.5Zm0 5.416V3.28h-.5v5.416h.5Zm-.245 1.382c.154-.466.245-.946.245-1.382h-.5c0 .37-.078.797-.22 1.226l.475.156Zm-1.825.172h1.587v-.5H19.18v.5Zm.098-1.602c0 .36-.126.823-.324 1.246l.452.213c.218-.464.372-1.002.372-1.459h-.5Zm0-5.415v5.415h.5V3.233h-.5Zm-.604-.605c.336 0 .604.268.604.605h.5a1.1 1.1 0 0 0-1.104-1.105v.5Zm-5.365 0h5.365v-.5H13.31v.5Zm.25-.203v-.047h-.5v.047h.5Zm-2.257 1.08c.205-.206.552-.416.939-.576.386-.159.78-.254 1.068-.254v-.5c-.377 0-.839.119-1.259.292-.42.173-.833.414-1.102.684l.354.354ZM2.85 11.96l8.452-8.456-.354-.354-8.451 8.456.353.354Zm0 .834a.582.582 0 0 1 0-.834l-.353-.354c-.43.43-.43 1.111 0 1.541l.353-.353Zm6.315 6.318L2.85 12.795l-.353.353 6.314 6.319.354-.354Zm.82.014c-.178.208-.577.23-.82-.014l-.354.354c.422.422 1.162.443 1.554-.015l-.38-.325Zm1.832-1.833-1.819 1.82.354.352 1.818-1.819-.353-.353Zm.426 2.267v-2.09h-.5v2.09h.5Zm-.847.95.774-.774-.353-.353-.774.774.353.354Zm-1.79.739a2.51 2.51 0 0 0 1.79-.738l-.353-.354a2.01 2.01 0 0 1-1.438.592v.5ZM20.988 20v-5c0-.55-.45-1-1-1h-5.996c-.55 0-1 .45-1 1v5c0 .55.45 1 1 1h5.996c.55 0 1-.45 1-1Zm-2.998-2.5c0 .55-.45 1-1 1s-1-.45-1-1 .45-1 1-1 1 .45 1 1Z" fill="currentColor"/>
<path d="M19.489 16v-2.5c0-1.4-1.1-2.5-2.499-2.5s-2.498 1.1-2.498 2.5V16" stroke="currentColor" stroke-opacity=".8" stroke-miterlimit="10"/>
</svg>
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19.916 9.556c-.111-.111-.111-.223-.222-.334L16.356 5.89a1.077 1.077 0 0 0-1.558 0 1.073 1.073 0 0 0 0 1.556l1.446 1.444h-5.118c-.667 0-1.112.444-1.112 1.111s.445 1.111 1.112 1.111h5.118l-1.446 1.445a1.073 1.073 0 0 0 0 1.555c.223.222.556.334.779.334.223 0 .556-.112.779-.334l3.338-3.333c.111-.111.222-.222.222-.333a1.225 1.225 0 0 0 0-.89Z" fill="currentColor"/>
<path d="M13.908 16.778a8.135 8.135 0 0 1-3.894 1c-4.34 0-7.789-3.445-7.789-7.778s3.45-7.778 7.789-7.778c1.335 0 2.67.334 3.894 1 .556.334 1.224.111 1.558-.444.334-.556.111-1.222-.445-1.556C13.463.444 11.794 0 10.014 0A9.965 9.965 0 0 0 0 10c0 5.556 4.45 10 10.014 10a9.94 9.94 0 0 0 5.007-1.333c.556-.334.667-1 .445-1.556-.334-.444-1.002-.667-1.558-.333Z" fill="currentColor"/>
</svg>
<svg viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M19.4 11a8.4 8.4 0 1 1-16.8 0 8.4 8.4 0 0 1 16.8 0Zm1.6 0c0 5.523-4.477 10-10 10S1 16.523 1 11 5.477 1 11 1s10 4.477 10 10Zm-5.895-3.706A.916.916 0 1 1 16.4 8.589l-6.022 6.022a1.05 1.05 0 0 1-1.485 0l-3.2-3.199a.915.915 0 0 1 1.295-1.295l2.258 2.258a.55.55 0 0 0 .778 0l5.081-5.081Z" fill="currentColor"/>
<path d="m16.4 7.293-.141.142.141-.142Zm-1.295 0 .142.142-.142-.141ZM16.4 8.59l-.141-.142.141.142Zm-6.022 6.022.141.141-.141-.141Zm-4.684-3.199.141-.141-.141.141Zm0-1.295-.142-.141.142.141Zm1.294 0-.141.142.141-.142Zm2.258 2.258-.14.142.14-.142Zm.778 0-.141-.141.141.141ZM11 19.6a8.6 8.6 0 0 0 8.6-8.6h-.4a8.2 8.2 0 0 1-8.2 8.2v.4ZM2.4 11a8.6 8.6 0 0 0 8.6 8.6v-.4A8.2 8.2 0 0 1 2.8 11h-.4ZM11 2.4A8.6 8.6 0 0 0 2.4 11h.4A8.2 8.2 0 0 1 11 2.8v-.4Zm8.6 8.6A8.6 8.6 0 0 0 11 2.4v.4a8.2 8.2 0 0 1 8.2 8.2h.4ZM11 21.2c5.633 0 10.2-4.567 10.2-10.2h-.4c0 5.412-4.388 9.8-9.8 9.8v.4ZM.8 11c0 5.633 4.567 10.2 10.2 10.2v-.4c-5.412 0-9.8-4.388-9.8-9.8H.8ZM11 .8C5.367.8.8 5.367.8 11h.4c0-5.412 4.388-9.8 9.8-9.8V.8ZM21.2 11C21.2 5.367 16.633.8 11 .8v.4c5.412 0 9.8 4.388 9.8 9.8h.4Zm-4.659-3.848a1.116 1.116 0 0 0-1.577 0l.283.283a.716.716 0 0 1 1.012 0l.282-.283Zm0 1.578a1.116 1.116 0 0 0 0-1.578l-.282.283c.28.28.28.733 0 1.012l.283.283Zm-6.022 6.022 6.023-6.022-.283-.283-6.023 6.023.283.282Zm-1.767 0a1.25 1.25 0 0 0 1.767 0l-.283-.282a.85.85 0 0 1-1.202 0l-.282.282Zm-3.2-3.199 3.2 3.2.282-.283-3.199-3.2-.283.283Zm0-1.577a1.115 1.115 0 0 0 0 1.577l.283-.282a.715.715 0 0 1 0-1.012l-.283-.283Zm1.578 0a1.115 1.115 0 0 0-1.578 0l.283.283a.715.715 0 0 1 1.012 0l.283-.283Zm2.258 2.258L7.13 9.976l-.283.283 2.258 2.258.283-.283Zm.495 0a.35.35 0 0 1-.495 0l-.283.283a.75.75 0 0 0 1.06 0l-.282-.283Zm5.081-5.082-5.081 5.082.283.283 5.08-5.082-.282-.283Z" fill="currentColor"/>
</svg>
...@@ -42,12 +42,12 @@ import type { AddressesResponse, AddressesMetadataSearchResult, AddressesMetadat ...@@ -42,12 +42,12 @@ import type { AddressesResponse, AddressesMetadataSearchResult, AddressesMetadat
import type { AddressMetadataInfo, PublicTagTypesResponse } from 'types/api/addressMetadata'; import type { AddressMetadataInfo, PublicTagTypesResponse } from 'types/api/addressMetadata';
import type { import type {
ArbitrumL2MessagesResponse, ArbitrumL2MessagesResponse,
ArbitrumL2MessagesItem,
ArbitrumL2TxnBatch, ArbitrumL2TxnBatch,
ArbitrumL2TxnBatchesResponse, ArbitrumL2TxnBatchesResponse,
ArbitrumL2BatchTxs, ArbitrumL2BatchTxs,
ArbitrumL2BatchBlocks, ArbitrumL2BatchBlocks,
ArbitrumL2TxnBatchesItem, ArbitrumL2TxnBatchesItem,
ArbitrumLatestDepositsResponse,
} from 'types/api/arbitrumL2'; } from 'types/api/arbitrumL2';
import type { TxBlobs, Blob } from 'types/api/blobs'; import type { TxBlobs, Blob } from 'types/api/blobs';
import type { import type {
...@@ -129,7 +129,15 @@ import type { TxInterpretationResponse } from 'types/api/txInterpretation'; ...@@ -129,7 +129,15 @@ import type { TxInterpretationResponse } from 'types/api/txInterpretation';
import type { TTxsFilters, TTxsWithBlobsFilters } from 'types/api/txsFilters'; import type { TTxsFilters, TTxsWithBlobsFilters } from 'types/api/txsFilters';
import type { TxStateChanges } from 'types/api/txStateChanges'; import type { TxStateChanges } from 'types/api/txStateChanges';
import type { UserOpsResponse, UserOp, UserOpsFilters, UserOpsAccount } from 'types/api/userOps'; import type { UserOpsResponse, UserOp, UserOpsFilters, UserOpsAccount } from 'types/api/userOps';
import type { ValidatorsCountersResponse, ValidatorsFilters, ValidatorsResponse, ValidatorsSorting } from 'types/api/validators'; import type {
ValidatorsStabilityCountersResponse,
ValidatorsStabilityFilters,
ValidatorsStabilityResponse,
ValidatorsStabilitySorting,
ValidatorsBlackfortCountersResponse,
ValidatorsBlackfortResponse,
ValidatorsBlackfortSorting,
} from 'types/api/validators';
import type { VerifiedContractsSorting } from 'types/api/verifiedContracts'; import type { VerifiedContractsSorting } from 'types/api/verifiedContracts';
import type { WithdrawalsResponse, WithdrawalsCounters } from 'types/api/withdrawals'; import type { WithdrawalsResponse, WithdrawalsCounters } from 'types/api/withdrawals';
import type { import type {
...@@ -168,9 +176,6 @@ export const RESOURCES = { ...@@ -168,9 +176,6 @@ export const RESOURCES = {
user_info: { user_info: {
path: '/api/account/v2/user/info', path: '/api/account/v2/user/info',
}, },
email_resend: {
path: '/api/account/v2/email/resend',
},
custom_abi: { custom_abi: {
path: '/api/account/v2/user/custom_abis{/:id}', path: '/api/account/v2/user/custom_abis{/:id}',
pathParams: [ 'id' as const ], pathParams: [ 'id' as const ],
...@@ -228,6 +233,26 @@ export const RESOURCES = { ...@@ -228,6 +233,26 @@ export const RESOURCES = {
needAuth: true, needAuth: true,
}, },
// AUTH
auth_send_otp: {
path: '/api/account/v2/send_otp',
},
auth_confirm_otp: {
path: '/api/account/v2/confirm_otp',
},
auth_siwe_message: {
path: '/api/account/v2/siwe_message',
},
auth_siwe_verify: {
path: '/api/account/v2/authenticate_via_wallet',
},
auth_link_email: {
path: '/api/account/v2/email/link',
},
auth_link_address: {
path: '/api/account/v2/address/link',
},
// STATS MICROSERVICE API // STATS MICROSERVICE API
stats_counters: { stats_counters: {
path: '/api/v1/counters', path: '/api/v1/counters',
...@@ -962,14 +987,19 @@ export const RESOURCES = { ...@@ -962,14 +987,19 @@ export const RESOURCES = {
}, },
// VALIDATORS // VALIDATORS
validators: { validators_stability: {
path: '/api/v2/validators/:chainType', path: '/api/v2/validators/stability',
pathParams: [ 'chainType' as const ],
filterFields: [ 'address_hash' as const, 'state_filter' as const ], filterFields: [ 'address_hash' as const, 'state_filter' as const ],
}, },
validators_counters: { validators_stability_counters: {
path: '/api/v2/validators/:chainType/counters', path: '/api/v2/validators/stability/counters',
pathParams: [ 'chainType' as const ], },
validators_blackfort: {
path: '/api/v2/validators/blackfort',
filterFields: [],
},
validators_blackfort_counters: {
path: '/api/v2/validators/blackfort/counters',
}, },
// BLOBS // BLOBS
...@@ -1067,7 +1097,7 @@ export type PaginatedResources = 'blocks' | 'block_txs' | 'block_election_reward ...@@ -1067,7 +1097,7 @@ export type PaginatedResources = 'blocks' | 'block_txs' | 'block_election_reward
'zksync_l2_txn_batches' | 'zksync_l2_txn_batch_txs' | 'zksync_l2_txn_batches' | 'zksync_l2_txn_batch_txs' |
'withdrawals' | 'address_withdrawals' | 'block_withdrawals' | 'withdrawals' | 'address_withdrawals' | 'block_withdrawals' |
'watchlist' | 'private_tags_address' | 'private_tags_tx' | 'watchlist' | 'private_tags_address' | 'private_tags_tx' |
'domains_lookup' | 'addresses_lookup' | 'user_ops' | 'validators' | 'noves_address_history'; 'domains_lookup' | 'addresses_lookup' | 'user_ops' | 'validators_stability' | 'validators_blackfort' | 'noves_address_history';
export type PaginatedResponse<Q extends PaginatedResources> = ResourcePayload<Q>; export type PaginatedResponse<Q extends PaginatedResources> = ResourcePayload<Q>;
...@@ -1093,7 +1123,7 @@ Q extends 'homepage_blocks' ? Array<Block> : ...@@ -1093,7 +1123,7 @@ Q extends 'homepage_blocks' ? Array<Block> :
Q extends 'homepage_txs' ? Array<Transaction> : Q extends 'homepage_txs' ? Array<Transaction> :
Q extends 'homepage_txs_watchlist' ? Array<Transaction> : Q extends 'homepage_txs_watchlist' ? Array<Transaction> :
Q extends 'homepage_optimistic_deposits' ? Array<OptimisticL2DepositsItem> : Q extends 'homepage_optimistic_deposits' ? Array<OptimisticL2DepositsItem> :
Q extends 'homepage_arbitrum_deposits' ? { items: Array<ArbitrumL2MessagesItem> } : Q extends 'homepage_arbitrum_deposits' ? ArbitrumLatestDepositsResponse :
Q extends 'homepage_zkevm_l2_batches' ? { items: Array<ZkEvmL2TxnBatchesItem> } : Q extends 'homepage_zkevm_l2_batches' ? { items: Array<ZkEvmL2TxnBatchesItem> } :
Q extends 'homepage_arbitrum_l2_batches' ? { items: Array<ArbitrumL2TxnBatchesItem>} : Q extends 'homepage_arbitrum_l2_batches' ? { items: Array<ArbitrumL2TxnBatchesItem>} :
Q extends 'homepage_indexing_status' ? IndexingStatus : Q extends 'homepage_indexing_status' ? IndexingStatus :
...@@ -1188,8 +1218,10 @@ Q extends 'address_metadata_tag_types' ? PublicTagTypesResponse : ...@@ -1188,8 +1218,10 @@ Q extends 'address_metadata_tag_types' ? PublicTagTypesResponse :
Q extends 'blob' ? Blob : Q extends 'blob' ? Blob :
Q extends 'marketplace_dapps' ? Array<MarketplaceAppOverview> : Q extends 'marketplace_dapps' ? Array<MarketplaceAppOverview> :
Q extends 'marketplace_dapp' ? MarketplaceAppOverview : Q extends 'marketplace_dapp' ? MarketplaceAppOverview :
Q extends 'validators' ? ValidatorsResponse : Q extends 'validators_stability' ? ValidatorsStabilityResponse :
Q extends 'validators_counters' ? ValidatorsCountersResponse : Q extends 'validators_stability_counters' ? ValidatorsStabilityCountersResponse :
Q extends 'validators_blackfort' ? ValidatorsBlackfortResponse :
Q extends 'validators_blackfort_counters' ? ValidatorsBlackfortCountersResponse :
Q extends 'shibarium_withdrawals' ? ShibariumWithdrawalsResponse : Q extends 'shibarium_withdrawals' ? ShibariumWithdrawalsResponse :
Q extends 'shibarium_deposits' ? ShibariumDepositsResponse : Q extends 'shibarium_deposits' ? ShibariumDepositsResponse :
Q extends 'shibarium_withdrawals_count' ? number : Q extends 'shibarium_withdrawals_count' ? number :
...@@ -1274,7 +1306,7 @@ Q extends 'verified_contracts' ? VerifiedContractsFilters : ...@@ -1274,7 +1306,7 @@ Q extends 'verified_contracts' ? VerifiedContractsFilters :
Q extends 'addresses_lookup' ? EnsAddressLookupFilters : Q extends 'addresses_lookup' ? EnsAddressLookupFilters :
Q extends 'domains_lookup' ? EnsDomainLookupFilters : Q extends 'domains_lookup' ? EnsDomainLookupFilters :
Q extends 'user_ops' ? UserOpsFilters : Q extends 'user_ops' ? UserOpsFilters :
Q extends 'validators' ? ValidatorsFilters : Q extends 'validators_stability' ? ValidatorsStabilityFilters :
Q extends 'address_mud_tables' ? AddressMudTablesFilter : Q extends 'address_mud_tables' ? AddressMudTablesFilter :
Q extends 'address_mud_records' ? AddressMudRecordsFilter : Q extends 'address_mud_records' ? AddressMudRecordsFilter :
never; never;
...@@ -1288,7 +1320,8 @@ Q extends 'verified_contracts' ? VerifiedContractsSorting : ...@@ -1288,7 +1320,8 @@ Q extends 'verified_contracts' ? VerifiedContractsSorting :
Q extends 'address_txs' ? TransactionsSorting : Q extends 'address_txs' ? TransactionsSorting :
Q extends 'addresses_lookup' ? EnsLookupSorting : Q extends 'addresses_lookup' ? EnsLookupSorting :
Q extends 'domains_lookup' ? EnsLookupSorting : Q extends 'domains_lookup' ? EnsLookupSorting :
Q extends 'validators' ? ValidatorsSorting : Q extends 'validators_stability' ? ValidatorsStabilitySorting :
Q extends 'validators_blackfort' ? ValidatorsBlackfortSorting :
Q extends 'address_mud_records' ? AddressMudRecordsSorting : Q extends 'address_mud_records' ? AddressMudRecordsSorting :
never; never;
/* eslint-enable @typescript-eslint/indent */ /* eslint-enable @typescript-eslint/indent */
...@@ -10,7 +10,7 @@ type TMarketplaceContext = { ...@@ -10,7 +10,7 @@ type TMarketplaceContext = {
setIsAutoConnectDisabled: (isAutoConnectDisabled: boolean) => void; setIsAutoConnectDisabled: (isAutoConnectDisabled: boolean) => void;
} }
const MarketplaceContext = createContext<TMarketplaceContext>({ export const MarketplaceContext = createContext<TMarketplaceContext>({
isAutoConnectDisabled: false, isAutoConnectDisabled: false,
setIsAutoConnectDisabled: () => {}, setIsAutoConnectDisabled: () => {},
}); });
......
...@@ -7,8 +7,6 @@ export enum NAMES { ...@@ -7,8 +7,6 @@ export enum NAMES {
API_TOKEN='_explorer_key', API_TOKEN='_explorer_key',
REWARDS_API_TOKEN='rewards_api_token', REWARDS_API_TOKEN='rewards_api_token',
REWARDS_REFERRAL_CODE='rewards_ref_code', REWARDS_REFERRAL_CODE='rewards_ref_code',
INVALID_SESSION='invalid_session',
CONFIRM_EMAIL_PAGE_VIEWED='confirm_email_page_viewed',
TXS_SORT='txs_sort', TXS_SORT='txs_sort',
COLOR_MODE='chakra-ui-color-mode', COLOR_MODE='chakra-ui-color-mode',
COLOR_MODE_HEX='chakra-ui-color-mode-hex', COLOR_MODE_HEX='chakra-ui-color-mode-hex',
...@@ -30,12 +28,16 @@ export function get(name?: NAMES | undefined | null, serverCookie?: string) { ...@@ -30,12 +28,16 @@ export function get(name?: NAMES | undefined | null, serverCookie?: string) {
} }
} }
export function set(name: string, value: string, attributes: Cookies.CookieAttributes = {}) { export function set(name: NAMES, value: string, attributes: Cookies.CookieAttributes = {}) {
attributes.path = '/'; attributes.path = '/';
return Cookies.set(name, value, attributes); return Cookies.set(name, value, attributes);
} }
export function remove(name: NAMES, attributes: Cookies.CookieAttributes = {}) {
return Cookies.remove(name, attributes);
}
export function getFromCookieString(cookieString: string, name?: NAMES | undefined | null) { export function getFromCookieString(cookieString: string, name?: NAMES | undefined | null) {
return cookieString.split(`${ name }=`)[1]?.split(';')[0]; return cookieString.split(`${ name }=`)[1]?.split(';')[0];
} }
import getErrorObj from './getErrorObj';
export default function getErrorMessage(error: unknown): string | undefined {
const errorObj = getErrorObj(error);
return errorObj && 'message' in errorObj && typeof errorObj.message === 'string' ? errorObj.message : undefined;
}
import { useQuery } from '@tanstack/react-query';
import * as v from 'valibot';
import config from 'configs/app';
import type { ResourceError } from 'lib/api/resources';
import useFetch from 'lib/hooks/useFetch';
const feature = config.features.addressProfileAPI;
type AddressInfoApiQueryResponse = v.InferOutput<typeof AddressInfoSchema>;
const AddressInfoSchema = v.object({
user_profile: v.object({
username: v.union([ v.string(), v.null() ]),
}),
});
const ERROR_NAME = 'Invalid response schema';
export default function useAddressProfileApiQuery(hash: string | undefined, isEnabled = true) {
const fetch = useFetch();
return useQuery<unknown, ResourceError<unknown>, AddressInfoApiQueryResponse>({
queryKey: [ 'username_api', hash ],
queryFn: async() => {
if (!feature.isEnabled || !hash) {
return Promise.reject();
}
return fetch(feature.apiUrlTemplate.replace('{address}', hash), undefined, { omitSentryErrorLog: true });
},
enabled: isEnabled && Boolean(hash),
refetchOnMount: false,
select: (response) => {
const parsedResponse = v.safeParse(AddressInfoSchema, response);
if (!parsedResponse.success) {
throw Error(ERROR_NAME);
}
return parsedResponse.output;
},
});
}
...@@ -10,7 +10,7 @@ import useFetch from 'lib/hooks/useFetch'; ...@@ -10,7 +10,7 @@ import useFetch from 'lib/hooks/useFetch';
export default function useGetCsrfToken() { export default function useGetCsrfToken() {
const nodeApiFetch = useFetch(); const nodeApiFetch = useFetch();
useQuery({ return useQuery({
queryKey: getResourceKey('csrf'), queryKey: getResourceKey('csrf'),
queryFn: async() => { queryFn: async() => {
if (!isNeedProxy()) { if (!isNeedProxy()) {
......
import { useQueryClient } from '@tanstack/react-query';
import React from 'react';
import type { UserInfo } from 'types/api/account';
import { resourceKey } from 'lib/api/resources';
import useLoginUrl from 'lib/hooks/useLoginUrl';
export default function useIsAccountActionAllowed() {
const queryClient = useQueryClient();
const profileData = queryClient.getQueryData<UserInfo>([ resourceKey('user_info') ]);
const isAuth = Boolean(profileData);
const loginUrl = useLoginUrl();
return React.useCallback(() => {
if (!loginUrl) {
return false;
}
if (!isAuth) {
window.location.assign(loginUrl);
return false;
}
return true;
}, [ isAuth, loginUrl ]);
}
import { useRouter } from 'next/router';
import { route } from 'nextjs-routes';
import config from 'configs/app';
const feature = config.features.account;
export default function useLoginUrl() {
const router = useRouter();
return feature.isEnabled ?
feature.authUrl + route({ pathname: '/auth/auth0', query: { path: router.asPath } }) :
undefined;
}
...@@ -6,12 +6,10 @@ import type { NavItemInternal, NavItem, NavGroupItem } from 'types/client/naviga ...@@ -6,12 +6,10 @@ import type { NavItemInternal, NavItem, NavGroupItem } from 'types/client/naviga
import config from 'configs/app'; import config from 'configs/app';
import { useRewardsContext } from 'lib/contexts/rewards'; import { useRewardsContext } from 'lib/contexts/rewards';
import { rightLineArrow } from 'lib/html-entities'; import { rightLineArrow } from 'lib/html-entities';
import UserAvatar from 'ui/shared/UserAvatar';
interface ReturnType { interface ReturnType {
mainNavItems: Array<NavItem | NavGroupItem>; mainNavItems: Array<NavItem | NavGroupItem>;
accountNavItems: Array<NavItem>; accountNavItems: Array<NavItem>;
profileItem: NavItem;
} }
export function isGroupItem(item: NavItem | NavGroupItem): item is NavGroupItem { export function isGroupItem(item: NavItem | NavGroupItem): item is NavGroupItem {
...@@ -255,7 +253,7 @@ export default function useNavItems(): ReturnType { ...@@ -255,7 +253,7 @@ export default function useNavItems(): ReturnType {
text: 'Charts & stats', text: 'Charts & stats',
nextRoute: { pathname: '/stats' as const }, nextRoute: { pathname: '/stats' as const },
icon: 'stats', icon: 'stats',
isActive: pathname === '/stats', isActive: pathname.startsWith('/stats'),
} : null, } : null,
apiNavItems.length > 0 && { apiNavItems.length > 0 && {
text: 'API', text: 'API',
...@@ -311,13 +309,6 @@ export default function useNavItems(): ReturnType { ...@@ -311,13 +309,6 @@ export default function useNavItems(): ReturnType {
}, },
].filter(Boolean); ].filter(Boolean);
const profileItem = { return { mainNavItems, accountNavItems };
text: 'My profile',
nextRoute: { pathname: '/auth/profile' as const },
iconComponent: UserAvatar,
isActive: pathname === '/auth/profile',
};
return { mainNavItems, accountNavItems, profileItem };
}, [ pathname, openRewardsLoginModal, rewardsBalance, dailyReward, rewardsApiToken ]); }, [ pathname, openRewardsLoginModal, rewardsBalance, dailyReward, rewardsApiToken ]);
} }
...@@ -20,7 +20,7 @@ export default function generate<Pathname extends Route['pathname']>(route: Rout ...@@ -20,7 +20,7 @@ export default function generate<Pathname extends Route['pathname']>(route: Rout
}; };
const title = compileValue(templates.title.make(route.pathname, Boolean(apiData)), params); const title = compileValue(templates.title.make(route.pathname, Boolean(apiData)), params);
const description = compileValue(templates.description.make(route.pathname), params); const description = compileValue(templates.description.make(route.pathname, Boolean(apiData)), params);
const pageOgType = getPageOgType(route.pathname); const pageOgType = getPageOgType(route.pathname);
......
...@@ -23,6 +23,7 @@ const OG_TYPE_DICT: Record<Route['pathname'], OGPageType> = { ...@@ -23,6 +23,7 @@ const OG_TYPE_DICT: Record<Route['pathname'], OGPageType> = {
'/apps': 'Root page', '/apps': 'Root page',
'/apps/[id]': 'Regular page', '/apps/[id]': 'Regular page',
'/stats': 'Root page', '/stats': 'Root page',
'/stats/[id]': 'Regular page',
'/api-docs': 'Regular page', '/api-docs': 'Regular page',
'/graphiql': 'Regular page', '/graphiql': 'Regular page',
'/search-results': 'Regular page', '/search-results': 'Regular page',
...@@ -64,8 +65,6 @@ const OG_TYPE_DICT: Record<Route['pathname'], OGPageType> = { ...@@ -64,8 +65,6 @@ const OG_TYPE_DICT: Record<Route['pathname'], OGPageType> = {
'/api/healthz': 'Regular page', '/api/healthz': 'Regular page',
'/api/config': 'Regular page', '/api/config': 'Regular page',
'/api/sprite': 'Regular page', '/api/sprite': 'Regular page',
'/auth/auth0': 'Regular page',
'/auth/unverified-email': 'Regular page',
}; };
export default function getPageOgType(pathname: Route['pathname']) { export default function getPageOgType(pathname: Route['pathname']) {
......
...@@ -27,6 +27,7 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = { ...@@ -27,6 +27,7 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
'/apps': DEFAULT_TEMPLATE, '/apps': DEFAULT_TEMPLATE,
'/apps/[id]': DEFAULT_TEMPLATE, '/apps/[id]': DEFAULT_TEMPLATE,
'/stats': DEFAULT_TEMPLATE, '/stats': DEFAULT_TEMPLATE,
'/stats/[id]': DEFAULT_TEMPLATE,
'/api-docs': DEFAULT_TEMPLATE, '/api-docs': DEFAULT_TEMPLATE,
'/graphiql': DEFAULT_TEMPLATE, '/graphiql': DEFAULT_TEMPLATE,
'/search-results': DEFAULT_TEMPLATE, '/search-results': DEFAULT_TEMPLATE,
...@@ -68,12 +69,12 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = { ...@@ -68,12 +69,12 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
'/api/healthz': DEFAULT_TEMPLATE, '/api/healthz': DEFAULT_TEMPLATE,
'/api/config': DEFAULT_TEMPLATE, '/api/config': DEFAULT_TEMPLATE,
'/api/sprite': DEFAULT_TEMPLATE, '/api/sprite': DEFAULT_TEMPLATE,
'/auth/auth0': DEFAULT_TEMPLATE,
'/auth/unverified-email': DEFAULT_TEMPLATE,
}; };
export function make(pathname: Route['pathname']) { const TEMPLATE_MAP_ENHANCED: Partial<Record<Route['pathname'], string>> = {
const template = TEMPLATE_MAP[pathname]; '/stats/[id]': '%description%',
};
return template ?? ''; export function make(pathname: Route['pathname'], isEnriched = false) {
return (isEnriched ? TEMPLATE_MAP_ENHANCED[pathname] : undefined) ?? TEMPLATE_MAP[pathname] ?? '';
} }
...@@ -23,6 +23,7 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = { ...@@ -23,6 +23,7 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
'/apps': '%network_name% DApps - Explore top apps', '/apps': '%network_name% DApps - Explore top apps',
'/apps/[id]': '%network_name% marketplace app', '/apps/[id]': '%network_name% marketplace app',
'/stats': '%network_name% stats - %network_name% network insights', '/stats': '%network_name% stats - %network_name% network insights',
'/stats/[id]': '%network_name% stats - %id% chart',
'/api-docs': '%network_name% API docs - %network_name% developer tools', '/api-docs': '%network_name% API docs - %network_name% developer tools',
'/graphiql': 'GraphQL for %network_name% - %network_name% data query', '/graphiql': 'GraphQL for %network_name% - %network_name% data query',
'/search-results': '%network_name% search result for %q%', '/search-results': '%network_name% search result for %q%',
...@@ -64,8 +65,6 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = { ...@@ -64,8 +65,6 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
'/api/healthz': '%network_name% node API health check', '/api/healthz': '%network_name% node API health check',
'/api/config': '%network_name% node API app config', '/api/config': '%network_name% node API app config',
'/api/sprite': '%network_name% node API SVG sprite content', '/api/sprite': '%network_name% node API SVG sprite content',
'/auth/auth0': '%network_name% authentication',
'/auth/unverified-email': '%network_name% unverified email',
}; };
const TEMPLATE_MAP_ENHANCED: Partial<Record<Route['pathname'], string>> = { const TEMPLATE_MAP_ENHANCED: Partial<Record<Route['pathname'], string>> = {
...@@ -73,6 +72,7 @@ const TEMPLATE_MAP_ENHANCED: Partial<Record<Route['pathname'], string>> = { ...@@ -73,6 +72,7 @@ const TEMPLATE_MAP_ENHANCED: Partial<Record<Route['pathname'], string>> = {
'/token/[hash]/instance/[id]': '%network_name% token instance for %symbol%', '/token/[hash]/instance/[id]': '%network_name% token instance for %symbol%',
'/apps/[id]': '%network_name% - %app_name%', '/apps/[id]': '%network_name% - %app_name%',
'/address/[hash]': '%network_name% address details for %domain_name%', '/address/[hash]': '%network_name% address details for %domain_name%',
'/stats/[id]': '%title% chart on %network_name%',
}; };
export function make(pathname: Route['pathname'], isEnriched = false) { export function make(pathname: Route['pathname'], isEnriched = false) {
......
import type { LineChart } from '@blockscout/stats-types';
import type { TokenInfo } from 'types/api/token'; import type { TokenInfo } from 'types/api/token';
import type { Route } from 'nextjs-routes'; import type { Route } from 'nextjs-routes';
...@@ -9,6 +10,7 @@ export type ApiData<Pathname extends Route['pathname']> = ...@@ -9,6 +10,7 @@ export type ApiData<Pathname extends Route['pathname']> =
Pathname extends '/token/[hash]' ? TokenInfo : Pathname extends '/token/[hash]' ? TokenInfo :
Pathname extends '/token/[hash]/instance/[id]' ? { symbol: string } : Pathname extends '/token/[hash]/instance/[id]' ? { symbol: string } :
Pathname extends '/apps/[id]' ? { app_name: string } : Pathname extends '/apps/[id]' ? { app_name: string } :
Pathname extends '/stats/[id]' ? LineChart['info'] :
never never
) | null; ) | null;
......
...@@ -21,6 +21,7 @@ export const PAGE_TYPE_DICT: Record<Route['pathname'], string> = { ...@@ -21,6 +21,7 @@ export const PAGE_TYPE_DICT: Record<Route['pathname'], string> = {
'/apps': 'DApps', '/apps': 'DApps',
'/apps/[id]': 'DApp', '/apps/[id]': 'DApp',
'/stats': 'Stats', '/stats': 'Stats',
'/stats/[id]': 'Stats chart',
'/api-docs': 'REST API', '/api-docs': 'REST API',
'/graphiql': 'GraphQL', '/graphiql': 'GraphQL',
'/search-results': 'Search results', '/search-results': 'Search results',
...@@ -62,8 +63,6 @@ export const PAGE_TYPE_DICT: Record<Route['pathname'], string> = { ...@@ -62,8 +63,6 @@ export const PAGE_TYPE_DICT: Record<Route['pathname'], string> = {
'/api/healthz': 'Node API: Health check', '/api/healthz': 'Node API: Health check',
'/api/config': 'Node API: App config', '/api/config': 'Node API: App config',
'/api/sprite': 'Node API: SVG sprite content', '/api/sprite': 'Node API: SVG sprite content',
'/auth/auth0': 'Auth',
'/auth/unverified-email': 'Unverified email',
}; };
export default function getPageType(pathname: Route['pathname']) { export default function getPageType(pathname: Route['pathname']) {
......
...@@ -7,6 +7,8 @@ export enum EventTypes { ...@@ -7,6 +7,8 @@ export enum EventTypes {
LOCAL_SEARCH = 'Local search', LOCAL_SEARCH = 'Local search',
ADD_TO_WALLET = 'Add to wallet', ADD_TO_WALLET = 'Add to wallet',
ACCOUNT_ACCESS = 'Account access', ACCOUNT_ACCESS = 'Account access',
LOGIN = 'Login',
ACCOUNT_LINK_INFO = 'Account link info',
PRIVATE_TAG = 'Private tag', PRIVATE_TAG = 'Private tag',
VERIFY_ADDRESS = 'Verify address', VERIFY_ADDRESS = 'Verify address',
VERIFY_TOKEN = 'Verify token', VERIFY_TOKEN = 'Verify token',
...@@ -54,7 +56,27 @@ Type extends EventTypes.ADD_TO_WALLET ? ( ...@@ -54,7 +56,27 @@ Type extends EventTypes.ADD_TO_WALLET ? (
} }
) : ) :
Type extends EventTypes.ACCOUNT_ACCESS ? { Type extends EventTypes.ACCOUNT_ACCESS ? {
'Action': 'Auth0 init' | 'Verification email resent' | 'Logged out'; 'Action': 'Dropdown open' | 'Logged out';
} :
Type extends EventTypes.LOGIN ? (
{
'Action': 'Started';
'Source': string;
} | {
'Action': 'Wallet' | 'Email';
'Source': 'Options selector';
} | {
'Action': 'OTP sent';
'Source': 'Email';
} | {
'Action': 'Success';
'Source': 'Email' | 'Wallet';
}
) :
Type extends EventTypes.ACCOUNT_LINK_INFO ? {
'Source': 'Profile' | 'Login modal' | 'Profile dropdown';
'Status': 'Started' | 'OTP sent' | 'Finished';
'Type': 'Email' | 'Wallet';
} : } :
Type extends EventTypes.PRIVATE_TAG ? { Type extends EventTypes.PRIVATE_TAG ? {
'Action': 'Form opened' | 'Submit'; 'Action': 'Form opened' | 'Submit';
...@@ -75,7 +97,7 @@ Type extends EventTypes.VERIFY_TOKEN ? { ...@@ -75,7 +97,7 @@ Type extends EventTypes.VERIFY_TOKEN ? {
'Action': 'Form opened' | 'Submit'; 'Action': 'Form opened' | 'Submit';
} : } :
Type extends EventTypes.WALLET_CONNECT ? { Type extends EventTypes.WALLET_CONNECT ? {
'Source': 'Header' | 'Smart contracts' | 'Swap button' | 'Merits'; 'Source': 'Header' | 'Login' | 'Profile' | 'Profile dropdown' | 'Smart contracts' | 'Swap button' | 'Merits';
'Status': 'Started' | 'Connected'; 'Status': 'Started' | 'Connected';
} : } :
Type extends EventTypes.WALLET_ACTION ? ( Type extends EventTypes.WALLET_ACTION ? (
...@@ -116,6 +138,9 @@ Type extends EventTypes.PAGE_WIDGET ? ( ...@@ -116,6 +138,9 @@ Type extends EventTypes.PAGE_WIDGET ? (
'Type': 'Address tag'; 'Type': 'Address tag';
'Info': string; 'Info': string;
'URL': string; 'URL': string;
} | {
'Type': 'Share chart';
'Info': string;
} }
) : ) :
Type extends EventTypes.TX_INTERPRETATION_INTERACTION ? { Type extends EventTypes.TX_INTERPRETATION_INTERACTION ? {
......
import React from 'react';
import config from 'configs/app';
import useApiQuery from 'lib/api/useApiQuery';
import useAccount from './useAccount';
export default function useAccountWithDomain(isEnabled: boolean) {
const { address } = useAccount();
const isQueryEnabled = config.features.nameService.isEnabled && Boolean(address) && Boolean(isEnabled);
const domainQuery = useApiQuery('address_domain', {
pathParams: {
chainId: config.chain.id,
address,
},
queryOptions: {
enabled: isQueryEnabled,
refetchOnMount: false,
},
});
return React.useMemo(() => {
return {
address: isEnabled ? address : undefined,
domain: domainQuery.data?.domain?.name,
isLoading: isQueryEnabled && domainQuery.isLoading,
};
}, [ address, domainQuery.data?.domain?.name, domainQuery.isLoading, isEnabled, isQueryEnabled ]);
}
...@@ -8,11 +8,11 @@ interface Params { ...@@ -8,11 +8,11 @@ interface Params {
source: mixpanel.EventPayload<mixpanel.EventTypes.WALLET_CONNECT>['Source']; source: mixpanel.EventPayload<mixpanel.EventTypes.WALLET_CONNECT>['Source'];
} }
export default function useWallet({ source }: Params) { export default function useWeb3Wallet({ source }: Params) {
const { open } = useWeb3Modal(); const { open: openModal } = useWeb3Modal();
const { open: isOpen } = useWeb3ModalState(); const { open: isOpen } = useWeb3ModalState();
const { disconnect } = useDisconnect(); const { disconnect } = useDisconnect();
const [ isModalOpening, setIsModalOpening ] = React.useState(false); const [ isOpening, setIsOpening ] = React.useState(false);
const [ isClientLoaded, setIsClientLoaded ] = React.useState(false); const [ isClientLoaded, setIsClientLoaded ] = React.useState(false);
const isConnectionStarted = React.useRef(false); const isConnectionStarted = React.useRef(false);
...@@ -21,12 +21,12 @@ export default function useWallet({ source }: Params) { ...@@ -21,12 +21,12 @@ export default function useWallet({ source }: Params) {
}, []); }, []);
const handleConnect = React.useCallback(async() => { const handleConnect = React.useCallback(async() => {
setIsModalOpening(true); setIsOpening(true);
await open(); await openModal();
setIsModalOpening(false); setIsOpening(false);
mixpanel.logEvent(mixpanel.EventTypes.WALLET_CONNECT, { Source: source, Status: 'Started' }); mixpanel.logEvent(mixpanel.EventTypes.WALLET_CONNECT, { Source: source, Status: 'Started' });
isConnectionStarted.current = true; isConnectionStarted.current = true;
}, [ open, source ]); }, [ openModal, source ]);
const handleAccountConnected = React.useCallback(({ isReconnected }: { isReconnected: boolean }) => { const handleAccountConnected = React.useCallback(({ isReconnected }: { isReconnected: boolean }) => {
if (!isReconnected && isConnectionStarted.current) { if (!isReconnected && isConnectionStarted.current) {
...@@ -46,15 +46,14 @@ export default function useWallet({ source }: Params) { ...@@ -46,15 +46,14 @@ export default function useWallet({ source }: Params) {
const { address, isDisconnected } = useAccount(); const { address, isDisconnected } = useAccount();
const isWalletConnected = isClientLoaded && !isDisconnected && address !== undefined; const isConnected = isClientLoaded && !isDisconnected && address !== undefined;
return { return {
openModal: open,
isWalletConnected,
address: address || '',
connect: handleConnect, connect: handleConnect,
disconnect: handleDisconnect, disconnect: handleDisconnect,
isModalOpening, isOpen: isOpening || isOpen,
isModalOpen: isOpen, isConnected,
address,
openModal,
}; };
} }
import type { ArbitrumL2MessagesResponse } from 'types/api/arbitrumL2'; import type { ArbitrumL2MessagesResponse, ArbitrumLatestDepositsResponse } from 'types/api/arbitrumL2';
export const baseResponse: ArbitrumL2MessagesResponse = { export const baseResponse: ArbitrumL2MessagesResponse = {
items: [ items: [
...@@ -27,3 +27,20 @@ export const baseResponse: ArbitrumL2MessagesResponse = { ...@@ -27,3 +27,20 @@ export const baseResponse: ArbitrumL2MessagesResponse = {
direction: 'to-rollup', direction: 'to-rollup',
}, },
}; };
export const latestDepositsResponse: ArbitrumLatestDepositsResponse = {
items: [
{
completion_transaction_hash: '0x3ccdf87449d3de6a9dcd3eddb7bc9ecdf1770d4631f03cdf12a098911618d138',
origination_transaction_block_number: 123400,
origination_transaction_hash: '0x210d9f70f411de1079e32a98473b04345a5ea6ff2340a8511ebc2df641274436',
origination_timestamp: '2023-06-01T14:46:48.000000Z',
},
{
completion_transaction_hash: '0xd16d918b2f95a5cdf66824f6291b6d5eb80b6f4acab3f9fb82ee0ec4109646a0',
origination_timestamp: null,
origination_transaction_block_number: null,
origination_transaction_hash: null,
},
],
};
...@@ -4,158 +4,195 @@ export const averageGasPrice: stats.LineChart = { ...@@ -4,158 +4,195 @@ export const averageGasPrice: stats.LineChart = {
chart: [ chart: [
{ {
date: '2023-12-22', date: '2023-12-22',
date_to: '2023-12-22',
value: '37.7804422597599', value: '37.7804422597599',
is_approximate: false, is_approximate: false,
}, },
{ {
date: '2023-12-23', date: '2023-12-23',
date_to: '2023-12-23',
value: '25.84889883009387', value: '25.84889883009387',
is_approximate: false, is_approximate: false,
}, },
{ {
date: '2023-12-24', date: '2023-12-24',
date_to: '2023-12-24',
value: '25.818463227198574', value: '25.818463227198574',
is_approximate: false, is_approximate: false,
}, },
{ {
date: '2023-12-25', date: '2023-12-25',
date_to: '2023-12-25',
value: '26.045513050051298', value: '26.045513050051298',
is_approximate: false, is_approximate: false,
}, },
{ {
date: '2023-12-26', date: '2023-12-26',
date_to: '2023-12-26',
value: '21.42600692652399', value: '21.42600692652399',
is_approximate: false, is_approximate: false,
}, },
{ {
date: '2023-12-27', date: '2023-12-27',
date_to: '2023-12-27',
value: '31.066730409846656', value: '31.066730409846656',
is_approximate: false, is_approximate: false,
}, },
{ {
date: '2023-12-28', date: '2023-12-28',
date_to: '2023-12-28',
value: '33.63955781902089', value: '33.63955781902089',
is_approximate: false, is_approximate: false,
}, },
{ {
date: '2023-12-29', date: '2023-12-29',
date_to: '2023-12-29',
value: '28.064736756058384', value: '28.064736756058384',
is_approximate: false, is_approximate: false,
}, },
{ {
date: '2023-12-30', date: '2023-12-30',
date_to: '2023-12-30',
value: '23.074500869678175', value: '23.074500869678175',
is_approximate: false, is_approximate: false,
}, },
{ {
date: '2023-12-31', date: '2023-12-31',
date_to: '2023-12-31',
value: '17.651005734615133', value: '17.651005734615133',
is_approximate: false, is_approximate: false,
}, },
{ {
date: '2024-01-01', date: '2024-01-01',
date_to: '2023-01-01',
value: '14.906085174476441', value: '14.906085174476441',
is_approximate: false, is_approximate: false,
}, },
{ {
date: '2024-01-02', date: '2024-01-02',
date_to: '2023-01-02',
value: '22.28459059038656', value: '22.28459059038656',
is_approximate: false, is_approximate: false,
}, },
{ {
date: '2024-01-03', date: '2024-01-03',
date_to: '2023-01-03',
value: '39.8311646806592', value: '39.8311646806592',
is_approximate: false, is_approximate: false,
}, },
{ {
date: '2024-01-04', date: '2024-01-04',
date_to: '2023-01-04',
value: '26.09989322256083', value: '26.09989322256083',
is_approximate: false, is_approximate: false,
}, },
{ {
date: '2024-01-05', date: '2024-01-05',
date_to: '2023-01-05',
value: '22.821996688111998', value: '22.821996688111998',
is_approximate: false, is_approximate: false,
}, },
{ {
date: '2024-01-06', date: '2024-01-06',
date_to: '2023-01-06',
value: '20.32680041262083', value: '20.32680041262083',
is_approximate: false, is_approximate: false,
}, },
{ {
date: '2024-01-07', date: '2024-01-07',
date_to: '2023-01-07',
value: '32.535045831809704', value: '32.535045831809704',
is_approximate: false, is_approximate: false,
}, },
{ {
date: '2024-01-08', date: '2024-01-08',
date_to: '2023-01-08',
value: '27.443477102139482', value: '27.443477102139482',
is_approximate: false, is_approximate: false,
}, },
{ {
date: '2024-01-09', date: '2024-01-09',
date_to: '2023-01-09',
value: '20.7911332558055', value: '20.7911332558055',
is_approximate: false, is_approximate: false,
}, },
{ {
date: '2024-01-10', date: '2024-01-10',
date_to: '2023-01-10',
value: '42.10740192523919', value: '42.10740192523919',
is_approximate: false, is_approximate: false,
}, },
{ {
date: '2024-01-11', date: '2024-01-11',
date_to: '2023-01-11',
value: '35.75215680343582', value: '35.75215680343582',
is_approximate: false, is_approximate: false,
}, },
{ {
date: '2024-01-12', date: '2024-01-12',
date_to: '2023-01-12',
value: '27.430414798093253', value: '27.430414798093253',
is_approximate: false, is_approximate: false,
}, },
{ {
date: '2024-01-13', date: '2024-01-13',
date_to: '2023-01-13',
value: '20.170934096589875', value: '20.170934096589875',
is_approximate: false, is_approximate: false,
}, },
{ {
date: '2024-01-14', date: '2024-01-14',
date_to: '2023-01-14',
value: '38.79660984371034', value: '38.79660984371034',
is_approximate: false, is_approximate: false,
}, },
{ {
date: '2024-01-15', date: '2024-01-15',
date_to: '2023-01-15',
value: '26.140740484554204', value: '26.140740484554204',
is_approximate: false, is_approximate: false,
}, },
{ {
date: '2024-01-16', date: '2024-01-16',
date_to: '2023-01-16',
value: '36.708543184194156', value: '36.708543184194156',
is_approximate: false, is_approximate: false,
}, },
{ {
date: '2024-01-17', date: '2024-01-17',
date_to: '2023-01-17',
value: '40.325438794298876', value: '40.325438794298876',
is_approximate: false, is_approximate: false,
}, },
{ {
date: '2024-01-18', date: '2024-01-18',
date_to: '2023-01-18',
value: '37.55145309930694', value: '37.55145309930694',
is_approximate: false, is_approximate: false,
}, },
{ {
date: '2024-01-19', date: '2024-01-19',
date_to: '2023-01-19',
value: '33.271450114434664', value: '33.271450114434664',
is_approximate: false, is_approximate: false,
}, },
{ {
date: '2024-01-20', date: '2024-01-20',
date_to: '2023-01-20',
value: '19.303304377685638', value: '19.303304377685638',
is_approximate: false, is_approximate: false,
}, },
{ {
date: '2024-01-21', date: '2024-01-21',
date_to: '2023-01-21',
value: '14.375908594704976', value: '14.375908594704976',
is_approximate: false, is_approximate: false,
}, },
], ],
info: {
title: 'Chart title',
description: 'Chart description',
id: 'chart',
resolutions: [ 'DAY', 'MONTH' ],
},
}; };
...@@ -11,18 +11,21 @@ export const base: stats.LineCharts = { ...@@ -11,18 +11,21 @@ export const base: stats.LineCharts = {
title: 'Accounts growth', title: 'Accounts growth',
description: 'Cumulative accounts number per period', description: 'Cumulative accounts number per period',
units: undefined, units: undefined,
resolutions: [ 'DAY', 'MONTH' ],
}, },
{ {
id: 'activeAccounts', id: 'activeAccounts',
title: 'Active accounts', title: 'Active accounts',
description: 'Active accounts number per period', description: 'Active accounts number per period',
units: undefined, units: undefined,
resolutions: [ 'DAY', 'MONTH' ],
}, },
{ {
id: 'newAccounts', id: 'newAccounts',
title: 'New accounts', title: 'New accounts',
description: 'New accounts number per day', description: 'New accounts number per day',
units: undefined, units: undefined,
resolutions: [ 'DAY', 'MONTH' ],
}, },
], ],
}, },
...@@ -35,30 +38,35 @@ export const base: stats.LineCharts = { ...@@ -35,30 +38,35 @@ export const base: stats.LineCharts = {
title: 'Average transaction fee', title: 'Average transaction fee',
description: 'The average amount in ETH spent per transaction', description: 'The average amount in ETH spent per transaction',
units: 'ETH', units: 'ETH',
resolutions: [ 'DAY', 'MONTH' ],
}, },
{ {
id: 'newTxns', id: 'newTxns',
title: 'New transactions', title: 'New transactions',
description: 'New transactions number', description: 'New transactions number',
units: undefined, units: undefined,
resolutions: [ 'DAY', 'MONTH' ],
}, },
{ {
id: 'txnsFee', id: 'txnsFee',
title: 'Transactions fees', title: 'Transactions fees',
description: 'Amount of tokens paid as fees', description: 'Amount of tokens paid as fees',
units: 'ETH', units: 'ETH',
resolutions: [ 'DAY', 'MONTH' ],
}, },
{ {
id: 'txnsGrowth', id: 'txnsGrowth',
title: 'Transactions growth', title: 'Transactions growth',
description: 'Cumulative transactions number', description: 'Cumulative transactions number',
units: undefined, units: undefined,
resolutions: [ 'DAY', 'MONTH' ],
}, },
{ {
id: 'txnsSuccessRate', id: 'txnsSuccessRate',
title: 'Transactions success rate', title: 'Transactions success rate',
description: 'Successful transactions rate per day', description: 'Successful transactions rate per day',
units: undefined, units: undefined,
resolutions: [ 'DAY', 'MONTH' ],
}, },
], ],
}, },
...@@ -71,18 +79,21 @@ export const base: stats.LineCharts = { ...@@ -71,18 +79,21 @@ export const base: stats.LineCharts = {
title: 'Average block rewards', title: 'Average block rewards',
description: 'Average amount of distributed reward in tokens per day', description: 'Average amount of distributed reward in tokens per day',
units: 'ETH', units: 'ETH',
resolutions: [ 'DAY', 'MONTH' ],
}, },
{ {
id: 'averageBlockSize', id: 'averageBlockSize',
title: 'Average block size', title: 'Average block size',
description: 'Average size of blocks in bytes', description: 'Average size of blocks in bytes',
units: 'Bytes', units: 'Bytes',
resolutions: [ 'DAY', 'MONTH' ],
}, },
{ {
id: 'newBlocks', id: 'newBlocks',
title: 'New blocks', title: 'New blocks',
description: 'New blocks number', description: 'New blocks number',
units: undefined, units: undefined,
resolutions: [ 'DAY', 'MONTH' ],
}, },
], ],
}, },
...@@ -95,6 +106,7 @@ export const base: stats.LineCharts = { ...@@ -95,6 +106,7 @@ export const base: stats.LineCharts = {
title: 'New ETH transfers', title: 'New ETH transfers',
description: 'New token transfers number for the period', description: 'New token transfers number for the period',
units: undefined, units: undefined,
resolutions: [ 'DAY', 'MONTH' ],
}, },
], ],
}, },
...@@ -107,18 +119,21 @@ export const base: stats.LineCharts = { ...@@ -107,18 +119,21 @@ export const base: stats.LineCharts = {
title: 'Average gas limit', title: 'Average gas limit',
description: 'Average gas limit per block for the period', description: 'Average gas limit per block for the period',
units: undefined, units: undefined,
resolutions: [ 'DAY', 'MONTH' ],
}, },
{ {
id: 'averageGasPrice', id: 'averageGasPrice',
title: 'Average gas price', title: 'Average gas price',
description: 'Average gas price for the period (Gwei)', description: 'Average gas price for the period (Gwei)',
units: 'Gwei', units: 'Gwei',
resolutions: [ 'DAY', 'MONTH' ],
}, },
{ {
id: 'gasUsedGrowth', id: 'gasUsedGrowth',
title: 'Gas used growth', title: 'Gas used growth',
description: 'Cumulative gas used for the period', description: 'Cumulative gas used for the period',
units: undefined, units: undefined,
resolutions: [ 'DAY', 'MONTH' ],
}, },
], ],
}, },
...@@ -131,12 +146,14 @@ export const base: stats.LineCharts = { ...@@ -131,12 +146,14 @@ export const base: stats.LineCharts = {
title: 'New verified contracts', title: 'New verified contracts',
description: 'New verified contracts number for the period', description: 'New verified contracts number for the period',
units: undefined, units: undefined,
resolutions: [ 'DAY', 'MONTH' ],
}, },
{ {
id: 'verifiedContractsGrowth', id: 'verifiedContractsGrowth',
title: 'Verified contracts growth', title: 'Verified contracts growth',
description: 'Cumulative number verified contracts for the period', description: 'Cumulative number verified contracts for the period',
units: undefined, units: undefined,
resolutions: [ 'DAY', 'MONTH' ],
}, },
], ],
}, },
......
export const base = { import type { UserInfo } from 'types/api/account';
export const base: UserInfo = {
avatar: 'https://avatars.githubusercontent.com/u/22130104', avatar: 'https://avatars.githubusercontent.com/u/22130104',
email: 'tom@ohhhh.me', email: 'tom@ohhhh.me',
name: 'tom goriunov', name: 'tom goriunov',
nickname: 'tom2drum', nickname: 'tom2drum',
address_hash: null,
}; };
export const withoutEmail = { export const withoutEmail: UserInfo = {
avatar: 'https://avatars.githubusercontent.com/u/22130104', avatar: 'https://avatars.githubusercontent.com/u/22130104',
email: null, email: null,
name: 'tom goriunov', name: 'tom goriunov',
nickname: 'tom2drum', nickname: 'tom2drum',
address_hash: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859',
}; };
import type {
ValidatorBlackfort,
ValidatorsBlackfortCountersResponse,
ValidatorsBlackfortResponse,
} from 'types/api/validators';
import * as addressMock from '../address/address';
export const validator1: ValidatorBlackfort = {
address: addressMock.withName,
name: 'testnet-3',
commission: 10,
delegated_amount: '0',
self_bonded_amount: '10000',
};
export const validator2: ValidatorBlackfort = {
address: addressMock.withEns,
name: 'GooseGanG GooseGanG GooseGanG GooseGanG GooseGanG GooseGanG GooseGanG',
commission: 5000,
delegated_amount: '10000',
self_bonded_amount: '100',
};
export const validator3: ValidatorBlackfort = {
address: addressMock.withoutName,
name: 'testnet-1',
commission: 0,
delegated_amount: '0',
self_bonded_amount: '10000',
};
export const validatorsResponse: ValidatorsBlackfortResponse = {
items: [ validator1, validator2, validator3 ],
next_page_params: null,
};
export const validatorsCountersResponse: ValidatorsBlackfortCountersResponse = {
new_validators_counter_24h: '11',
validators_counter: '140',
};
import type { Validator, ValidatorsCountersResponse, ValidatorsResponse } from 'types/api/validators'; import type {
ValidatorStability,
ValidatorsStabilityCountersResponse,
ValidatorsStabilityResponse,
} from 'types/api/validators';
import * as addressMock from '../address/address'; import * as addressMock from '../address/address';
export const validator1: Validator = { export const validator1: ValidatorStability = {
address: addressMock.withName, address: addressMock.withName,
blocks_validated_count: 7334224, blocks_validated_count: 7334224,
state: 'active', state: 'active',
}; };
export const validator2: Validator = { export const validator2: ValidatorStability = {
address: addressMock.withEns, address: addressMock.withEns,
blocks_validated_count: 8937453, blocks_validated_count: 8937453,
state: 'probation', state: 'probation',
}; };
export const validator3: Validator = { export const validator3: ValidatorStability = {
address: addressMock.withoutName, address: addressMock.withoutName,
blocks_validated_count: 1234, blocks_validated_count: 1234,
state: 'inactive', state: 'inactive',
}; };
export const validatorsResponse: ValidatorsResponse = { export const validatorsResponse: ValidatorsStabilityResponse = {
items: [ validator1, validator2, validator3 ], items: [ validator1, validator2, validator3 ],
next_page_params: null, next_page_params: null,
}; };
export const validatorsCountersResponse: ValidatorsCountersResponse = { export const validatorsCountersResponse: ValidatorsStabilityCountersResponse = {
active_validators_counter: '42', active_validators_counter: '42',
active_validators_percentage: 7.14, active_validators_percentage: 7.14,
new_validators_counter_24h: '11', new_validators_counter_24h: '11',
......
...@@ -16,6 +16,7 @@ function generateCspPolicy() { ...@@ -16,6 +16,7 @@ function generateCspPolicy() {
descriptors.monaco(), descriptors.monaco(),
descriptors.safe(), descriptors.safe(),
descriptors.sentry(), descriptors.sentry(),
descriptors.usernameApi(),
descriptors.walletConnect(), descriptors.walletConnect(),
); );
......
...@@ -3,7 +3,7 @@ import type CspDev from 'csp-dev'; ...@@ -3,7 +3,7 @@ import type CspDev from 'csp-dev';
import config from 'configs/app'; import config from 'configs/app';
export function googleReCaptcha(): CspDev.DirectiveDescriptor { export function googleReCaptcha(): CspDev.DirectiveDescriptor {
if (!config.services.reCaptcha.siteKey) { if (!config.services.reCaptchaV3.siteKey) {
return {}; return {};
} }
......
...@@ -11,4 +11,5 @@ export { mixpanel } from './mixpanel'; ...@@ -11,4 +11,5 @@ export { mixpanel } from './mixpanel';
export { monaco } from './monaco'; export { monaco } from './monaco';
export { safe } from './safe'; export { safe } from './safe';
export { sentry } from './sentry'; export { sentry } from './sentry';
export { usernameApi } from './usernameApi';
export { walletConnect } from './walletConnect'; export { walletConnect } from './walletConnect';
import type CspDev from 'csp-dev';
import config from 'configs/app';
const feature = config.features.addressProfileAPI;
export function usernameApi(): CspDev.DirectiveDescriptor {
if (!feature.isEnabled) {
return {};
}
const apiOrigin = (() => {
try {
const url = new URL(feature.apiUrlTemplate);
return url.origin;
} catch (error) {
return '';
}
})();
return {
'connect-src': [
apiOrigin,
],
};
}
...@@ -6,9 +6,9 @@ import type { RollupType } from 'types/client/rollup'; ...@@ -6,9 +6,9 @@ import type { RollupType } from 'types/client/rollup';
import type { Route } from 'nextjs-routes'; import type { Route } from 'nextjs-routes';
import config from 'configs/app'; import config from 'configs/app';
import isNeedProxy from 'lib/api/isNeedProxy';
const rollupFeature = config.features.rollup; const rollupFeature = config.features.rollup;
const adBannerFeature = config.features.adsBanner; const adBannerFeature = config.features.adsBanner;
import isNeedProxy from 'lib/api/isNeedProxy';
import type * as metadata from 'lib/metadata'; import type * as metadata from 'lib/metadata';
export interface Props<Pathname extends Route['pathname'] = never> { export interface Props<Pathname extends Route['pathname'] = never> {
......
import type { NextRequest } from 'next/server'; import type { NextRequest } from 'next/server';
import { NextResponse } from 'next/server'; import { NextResponse } from 'next/server';
import { route } from 'nextjs-routes';
import config from 'configs/app'; import config from 'configs/app';
import { DAY } from 'lib/consts';
import * as cookies from 'lib/cookies'; import * as cookies from 'lib/cookies';
export function account(req: NextRequest) { export function account(req: NextRequest) {
...@@ -25,37 +22,7 @@ export function account(req: NextRequest) { ...@@ -25,37 +22,7 @@ export function account(req: NextRequest) {
const isProfileRoute = req.nextUrl.pathname.includes('/auth/profile'); const isProfileRoute = req.nextUrl.pathname.includes('/auth/profile');
if ((isAccountRoute || isProfileRoute)) { if ((isAccountRoute || isProfileRoute)) {
const authUrl = feature.authUrl + route({ pathname: '/auth/auth0', query: { path: req.nextUrl.pathname } }); return NextResponse.redirect(config.app.baseUrl);
return NextResponse.redirect(authUrl);
}
}
// if user hasn't confirmed email yet
if (req.cookies.get(cookies.NAMES.INVALID_SESSION)) {
// if user has both cookies, make redirect to logout
if (apiTokenCookie) {
// yes, we could have checked that the current URL is not the logout URL, but we hadn't
// logout URL is always external URL in auth0.com sub-domain
// at least we hope so
const res = NextResponse.redirect(feature.logoutUrl);
res.cookies.delete(cookies.NAMES.CONFIRM_EMAIL_PAGE_VIEWED); // reset cookie to show email verification page again
return res;
}
// if user hasn't seen email verification page, make redirect to it
if (!req.cookies.get(cookies.NAMES.CONFIRM_EMAIL_PAGE_VIEWED)) {
if (!req.nextUrl.pathname.includes('/auth/unverified-email')) {
const url = config.app.baseUrl + route({ pathname: '/auth/unverified-email' });
const res = NextResponse.redirect(url);
res.cookies.set({
name: cookies.NAMES.CONFIRM_EMAIL_PAGE_VIEWED,
value: 'true',
expires: Date.now() + 7 * DAY,
});
return res;
}
} }
} }
} }
...@@ -29,9 +29,7 @@ declare module "nextjs-routes" { ...@@ -29,9 +29,7 @@ declare module "nextjs-routes" {
| StaticRoute<"/api-docs"> | StaticRoute<"/api-docs">
| DynamicRoute<"/apps/[id]", { "id": string }> | DynamicRoute<"/apps/[id]", { "id": string }>
| StaticRoute<"/apps"> | StaticRoute<"/apps">
| StaticRoute<"/auth/auth0">
| StaticRoute<"/auth/profile"> | StaticRoute<"/auth/profile">
| StaticRoute<"/auth/unverified-email">
| DynamicRoute<"/batches/[number]", { "number": string }> | DynamicRoute<"/batches/[number]", { "number": string }>
| StaticRoute<"/batches"> | StaticRoute<"/batches">
| DynamicRoute<"/blobs/[hash]", { "hash": string }> | DynamicRoute<"/blobs/[hash]", { "hash": string }>
...@@ -56,6 +54,7 @@ declare module "nextjs-routes" { ...@@ -56,6 +54,7 @@ declare module "nextjs-routes" {
| StaticRoute<"/public-tags/submit"> | StaticRoute<"/public-tags/submit">
| StaticRoute<"/search-results"> | StaticRoute<"/search-results">
| StaticRoute<"/sprite"> | StaticRoute<"/sprite">
| DynamicRoute<"/stats/[id]", { "id": string }>
| StaticRoute<"/stats"> | StaticRoute<"/stats">
| DynamicRoute<"/token/[hash]", { "hash": string }> | DynamicRoute<"/token/[hash]", { "hash": string }>
| DynamicRoute<"/token/[hash]/instance/[id]", { "hash": string; "id": string }> | DynamicRoute<"/token/[hash]/instance/[id]", { "hash": string; "id": string }>
......
...@@ -12,6 +12,7 @@ type Params<R extends ResourceName> = ( ...@@ -12,6 +12,7 @@ type Params<R extends ResourceName> = (
{ {
resource: R; resource: R;
pathParams?: ResourcePathParams<R>; pathParams?: ResourcePathParams<R>;
queryParams?: Record<string, string | number | undefined>;
} | { } | {
url: string; url: string;
route: string; route: string;
...@@ -22,12 +23,11 @@ type Params<R extends ResourceName> = ( ...@@ -22,12 +23,11 @@ type Params<R extends ResourceName> = (
export default async function fetchApi<R extends ResourceName = never, S = ResourcePayload<R>>(params: Params<R>): Promise<S | undefined> { export default async function fetchApi<R extends ResourceName = never, S = ResourcePayload<R>>(params: Params<R>): Promise<S | undefined> {
const controller = new AbortController(); const controller = new AbortController();
const timeout = setTimeout(() => { const timeout = setTimeout(() => {
controller.abort(); controller.abort();
}, params.timeout || SECOND); }, params.timeout || SECOND);
const url = 'url' in params ? params.url : buildUrl(params.resource, params.pathParams); const url = 'url' in params ? params.url : buildUrl(params.resource, params.pathParams, params.queryParams);
const route = 'route' in params ? params.route : RESOURCES[params.resource]['path']; const route = 'route' in params ? params.route : RESOURCES[params.resource]['path'];
const end = metrics?.apiRequestDuration.startTimer(); const end = metrics?.apiRequestDuration.startTimer();
......
...@@ -34,6 +34,7 @@ export default function fetchFactory( ...@@ -34,6 +34,7 @@ export default function fetchFactory(
message: 'API fetch via Next.js proxy', message: 'API fetch via Next.js proxy',
url, url,
// headers, // headers,
// init,
}); });
const body = (() => { const body = (() => {
......
...@@ -4,8 +4,8 @@ ...@@ -4,8 +4,8 @@
"private": false, "private": false,
"homepage": "https://github.com/blockscout/frontend#readme", "homepage": "https://github.com/blockscout/frontend#readme",
"engines": { "engines": {
"node": "20.11.0", "node": "20.17.0",
"npm": "10.2.4" "npm": "10.8.2"
}, },
"scripts": { "scripts": {
"dev": "./tools/scripts/dev.sh", "dev": "./tools/scripts/dev.sh",
...@@ -26,8 +26,8 @@ ...@@ -26,8 +26,8 @@
"svg:build-sprite": "icons build -i ./icons -o ./public/icons --optimize", "svg:build-sprite": "icons build -i ./icons -o ./public/icons --optimize",
"test:pw": "./tools/scripts/pw.sh", "test:pw": "./tools/scripts/pw.sh",
"test:pw:local": "export NODE_PATH=$(pwd)/node_modules && yarn test:pw", "test:pw:local": "export NODE_PATH=$(pwd)/node_modules && yarn test:pw",
"test:pw:docker": "docker run --rm --network host -v $(pwd):/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.41.1-focal ./tools/scripts/pw.docker.sh", "test:pw:docker": "docker run --rm --ipc=host -v $(pwd):/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.47.2-focal ./tools/scripts/pw.docker.sh",
"test:pw:docker:deps": "docker run --rm --network host -v $(pwd):/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.41.1-focal ./tools/scripts/pw.docker.deps.sh", "test:pw:docker:deps": "docker run --rm --ipc=host -v $(pwd):/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.47.2-focal ./tools/scripts/pw.docker.deps.sh",
"test:pw:ci": "yarn test:pw --project=$PW_PROJECT", "test:pw:ci": "yarn test:pw --project=$PW_PROJECT",
"test:pw:detect-affected": "node ./deploy/tools/affected-tests/index.js", "test:pw:detect-affected": "node ./deploy/tools/affected-tests/index.js",
"test:jest": "jest", "test:jest": "jest",
...@@ -38,7 +38,7 @@ ...@@ -38,7 +38,7 @@
}, },
"dependencies": { "dependencies": {
"@blockscout/bens-types": "1.4.1", "@blockscout/bens-types": "1.4.1",
"@blockscout/stats-types": "1.6.0", "@blockscout/stats-types": "2.0.0",
"@blockscout/visualizer-types": "0.2.0", "@blockscout/visualizer-types": "0.2.0",
"@chakra-ui/react": "2.7.1", "@chakra-ui/react": "2.7.1",
"@chakra-ui/theme-tools": "^2.0.18", "@chakra-ui/theme-tools": "^2.0.18",
...@@ -87,7 +87,7 @@ ...@@ -87,7 +87,7 @@
"magic-bytes.js": "1.8.0", "magic-bytes.js": "1.8.0",
"mixpanel-browser": "^2.47.0", "mixpanel-browser": "^2.47.0",
"monaco-editor": "^0.34.1", "monaco-editor": "^0.34.1",
"next": "14.2.9", "next": "14.2.13",
"nextjs-routes": "^1.0.8", "nextjs-routes": "^1.0.8",
"node-fetch": "^3.2.9", "node-fetch": "^3.2.9",
"papaparse": "^5.3.2", "papaparse": "^5.3.2",
...@@ -100,7 +100,7 @@ ...@@ -100,7 +100,7 @@
"react": "18.2.0", "react": "18.2.0",
"react-device-detect": "^2.2.3", "react-device-detect": "^2.2.3",
"react-dom": "18.2.0", "react-dom": "18.2.0",
"react-google-recaptcha": "^3.1.0", "react-google-recaptcha-v3": "1.10.1",
"react-hook-form": "^7.33.1", "react-hook-form": "^7.33.1",
"react-identicons": "^1.2.5", "react-identicons": "^1.2.5",
"react-intersection-observer": "^9.5.2", "react-intersection-observer": "^9.5.2",
...@@ -115,8 +115,8 @@ ...@@ -115,8 +115,8 @@
"xss": "^1.0.14" "xss": "^1.0.14"
}, },
"devDependencies": { "devDependencies": {
"@playwright/experimental-ct-react": "1.41.1", "@playwright/experimental-ct-react": "1.47.2",
"@playwright/test": "1.41.1", "@playwright/test": "1.47.2",
"@svgr/webpack": "^6.5.1", "@svgr/webpack": "^6.5.1",
"@tanstack/eslint-plugin-query": "^5.0.5", "@tanstack/eslint-plugin-query": "^5.0.5",
"@testing-library/react": "^14.0.0", "@testing-library/react": "^14.0.0",
...@@ -128,7 +128,7 @@ ...@@ -128,7 +128,7 @@
"@types/jest": "^29.2.0", "@types/jest": "^29.2.0",
"@types/js-cookie": "^3.0.2", "@types/js-cookie": "^3.0.2",
"@types/mixpanel-browser": "^2.38.1", "@types/mixpanel-browser": "^2.38.1",
"@types/node": "20.11.0", "@types/node": "20.16.7",
"@types/phoenix": "^1.5.4", "@types/phoenix": "^1.5.4",
"@types/qrcode": "^1.5.0", "@types/qrcode": "^1.5.0",
"@types/react": "18.0.9", "@types/react": "18.0.9",
...@@ -161,7 +161,7 @@ ...@@ -161,7 +161,7 @@
"ts-node": "^10.9.1", "ts-node": "^10.9.1",
"typescript": "5.4.2", "typescript": "5.4.2",
"vite-plugin-svgr": "^2.2.2", "vite-plugin-svgr": "^2.2.2",
"vite-tsconfig-paths": "^3.5.2", "vite-tsconfig-paths": "4.3.2",
"ws": "^8.17.1" "ws": "^8.17.1"
}, },
"lint-staged": { "lint-staged": {
......
...@@ -22,8 +22,13 @@ const handler = async(nextReq: NextApiRequest, nextRes: NextApiResponse) => { ...@@ -22,8 +22,13 @@ const handler = async(nextReq: NextApiRequest, nextRes: NextApiResponse) => {
); );
// proxy some headers from API // proxy some headers from API
nextRes.setHeader('x-request-id', apiRes.headers.get('x-request-id') || ''); const requestId = apiRes.headers.get('x-request-id');
nextRes.setHeader('set-cookie', apiRes.headers.get('set-cookie') || ''); requestId && nextRes.setHeader('x-request-id', requestId);
const setCookie = apiRes.headers.raw()['set-cookie'];
setCookie?.forEach((value) => {
nextRes.appendHeader('set-cookie', value);
});
nextRes.status(apiRes.status).send(apiRes.body); nextRes.status(apiRes.status).send(apiRes.body);
}; };
......
import type { NextPage } from 'next';
const Page: NextPage = () => {
return null;
};
export default Page;
export async function getServerSideProps() {
return {
notFound: true,
};
}
import type { NextPage } from 'next';
import React from 'react';
import PageNextJs from 'nextjs/PageNextJs';
import UnverifiedEmail from 'ui/pages/UnverifiedEmail';
const Page: NextPage = () => {
return (
<PageNextJs pathname="/auth/unverified-email">
<UnverifiedEmail/>
</PageNextJs>
);
};
export default Page;
export { account as getServerSideProps } from 'nextjs/getServerSideProps';
import type { GetServerSideProps, NextPage } from 'next';
import dynamic from 'next/dynamic';
import React from 'react';
import type { Route } from 'nextjs-routes';
import * as gSSP from 'nextjs/getServerSideProps';
import type { Props } from 'nextjs/getServerSideProps';
import PageNextJs from 'nextjs/PageNextJs';
import detectBotRequest from 'nextjs/utils/detectBotRequest';
import fetchApi from 'nextjs/utils/fetchApi';
import config from 'configs/app';
import dayjs from 'lib/date/dayjs';
import getQueryParamString from 'lib/router/getQueryParamString';
const Chart = dynamic(() => import('ui/pages/Chart'), { ssr: false });
const pathname: Route['pathname'] = '/stats/[id]';
const Page: NextPage<Props<typeof pathname>> = (props: Props<typeof pathname>) => {
return (
<PageNextJs pathname="/stats/[id]" query={ props.query } apiData={ props.apiData }>
<Chart/>
</PageNextJs>
);
};
export default Page;
export const getServerSideProps: GetServerSideProps<Props<typeof pathname>> = async(ctx) => {
const baseResponse = await gSSP.base<typeof pathname>(ctx);
if ('props' in baseResponse) {
if (
config.meta.seo.enhancedDataEnabled ||
(config.meta.og.enhancedDataEnabled && detectBotRequest(ctx.req)?.type === 'social_preview')
) {
const chartData = await fetchApi({
resource: 'stats_line',
pathParams: { id: getQueryParamString(ctx.query.id) },
queryParams: { from: dayjs().format('YYYY-MM-DD'), to: dayjs().format('YYYY-MM-DD') },
timeout: 1000,
});
(await baseResponse.props).apiData = chartData?.info ?? null;
}
}
return baseResponse;
};
...@@ -4,7 +4,21 @@ import React from 'react'; ...@@ -4,7 +4,21 @@ import React from 'react';
import PageNextJs from 'nextjs/PageNextJs'; import PageNextJs from 'nextjs/PageNextJs';
const Validators = dynamic(() => import('ui/pages/Validators'), { ssr: false }); import config from 'configs/app';
const validatorsFeature = config.features.validators;
const Validators = dynamic(() => {
if (validatorsFeature.isEnabled && validatorsFeature.chainType === 'stability') {
return import('ui/pages/ValidatorsStability');
}
if (validatorsFeature.isEnabled && validatorsFeature.chainType === 'blackfort') {
return import('ui/pages/ValidatorsBlackfort');
}
throw new Error('Validators feature is not enabled.');
}, { ssr: false });
const Page: NextPage = () => { const Page: NextPage = () => {
return ( return (
......
...@@ -50,7 +50,7 @@ const config: PlaywrightTestConfig = defineConfig({ ...@@ -50,7 +50,7 @@ const config: PlaywrightTestConfig = defineConfig({
ctViteConfig: { ctViteConfig: {
plugins: [ plugins: [
tsconfigPaths(), tsconfigPaths({ loose: true, ignoreConfigErrors: true }),
react(), react(),
svgr({ svgr({
exportAsDefault: true, exportAsDefault: true,
...@@ -82,6 +82,9 @@ const config: PlaywrightTestConfig = defineConfig({ ...@@ -82,6 +82,9 @@ const config: PlaywrightTestConfig = defineConfig({
// We don't call this function in TestApp and since we use useWeb3Modal() and useWeb3ModalState() hooks in the code, we have to mock the module // We don't call this function in TestApp and since we use useWeb3Modal() and useWeb3ModalState() hooks in the code, we have to mock the module
// Otherwise it will complain that createWeb3Modal() is no called before the hooks are used // Otherwise it will complain that createWeb3Modal() is no called before the hooks are used
{ find: /^@web3modal\/wagmi\/react$/, replacement: './playwright/mocks/modules/@web3modal/wagmi/react.js' }, { find: /^@web3modal\/wagmi\/react$/, replacement: './playwright/mocks/modules/@web3modal/wagmi/react.js' },
{ find: '/playwright/index.ts', replacement: './playwright/index.ts' },
{ find: '/playwright/envs.js', replacement: './playwright/envs.js' },
], ],
}, },
define: { define: {
......
...@@ -10,6 +10,7 @@ import type { Props as PageProps } from 'nextjs/getServerSideProps'; ...@@ -10,6 +10,7 @@ import type { Props as PageProps } from 'nextjs/getServerSideProps';
import config from 'configs/app'; import config from 'configs/app';
import { AppContextProvider } from 'lib/contexts/app'; import { AppContextProvider } from 'lib/contexts/app';
import { MarketplaceContext } from 'lib/contexts/marketplace';
import { SocketProvider } from 'lib/socket/context'; import { SocketProvider } from 'lib/socket/context';
import currentChain from 'lib/web3/currentChain'; import currentChain from 'lib/web3/currentChain';
import theme from 'theme/theme'; import theme from 'theme/theme';
...@@ -23,6 +24,10 @@ export type Props = { ...@@ -23,6 +24,10 @@ export type Props = {
appContext?: { appContext?: {
pageProps: PageProps; pageProps: PageProps;
}; };
marketplaceContext?: {
isAutoConnectDisabled: boolean;
setIsAutoConnectDisabled: (isAutoConnectDisabled: boolean) => void;
};
} }
const defaultAppContext = { const defaultAppContext = {
...@@ -35,6 +40,11 @@ const defaultAppContext = { ...@@ -35,6 +40,11 @@ const defaultAppContext = {
}, },
}; };
const defaultMarketplaceContext = {
isAutoConnectDisabled: false,
setIsAutoConnectDisabled: () => {},
};
const wagmiConfig = createConfig({ const wagmiConfig = createConfig({
chains: [ currentChain ], chains: [ currentChain ],
connectors: [ connectors: [
...@@ -49,7 +59,7 @@ const wagmiConfig = createConfig({ ...@@ -49,7 +59,7 @@ const wagmiConfig = createConfig({
}, },
}); });
const TestApp = ({ children, withSocket, appContext = defaultAppContext }: Props) => { const TestApp = ({ children, withSocket, appContext = defaultAppContext, marketplaceContext = defaultMarketplaceContext }: Props) => {
const [ queryClient ] = React.useState(() => new QueryClient({ const [ queryClient ] = React.useState(() => new QueryClient({
defaultOptions: { defaultOptions: {
queries: { queries: {
...@@ -64,11 +74,13 @@ const TestApp = ({ children, withSocket, appContext = defaultAppContext }: Props ...@@ -64,11 +74,13 @@ const TestApp = ({ children, withSocket, appContext = defaultAppContext }: Props
<QueryClientProvider client={ queryClient }> <QueryClientProvider client={ queryClient }>
<SocketProvider url={ withSocket ? `ws://${ config.app.host }:${ socketPort }` : undefined }> <SocketProvider url={ withSocket ? `ws://${ config.app.host }:${ socketPort }` : undefined }>
<AppContextProvider { ...appContext }> <AppContextProvider { ...appContext }>
<GrowthBookProvider> <MarketplaceContext.Provider value={ marketplaceContext }>
<WagmiProvider config={ wagmiConfig }> <GrowthBookProvider>
{ children } <WagmiProvider config={ wagmiConfig }>
</WagmiProvider> { children }
</GrowthBookProvider> </WagmiProvider>
</GrowthBookProvider>
</MarketplaceContext.Provider>
</AppContextProvider> </AppContextProvider>
</SocketProvider> </SocketProvider>
</QueryClientProvider> </QueryClientProvider>
......
...@@ -4,8 +4,6 @@ import type { Locator, TestFixture } from '@playwright/test'; ...@@ -4,8 +4,6 @@ import type { Locator, TestFixture } from '@playwright/test';
import type router from 'next/router'; import type router from 'next/router';
import React from 'react'; import React from 'react';
import type { JsonObject } from '@playwright/experimental-ct-core/types/component';
import type { Props as TestAppProps } from 'playwright/TestApp'; import type { Props as TestAppProps } from 'playwright/TestApp';
import TestApp from 'playwright/TestApp'; import TestApp from 'playwright/TestApp';
...@@ -14,15 +12,13 @@ interface MountResult extends Locator { ...@@ -14,15 +12,13 @@ interface MountResult extends Locator {
update(component: JSX.Element): Promise<void>; update(component: JSX.Element): Promise<void>;
} }
type Mount = <HooksConfig extends JsonObject>(component: JSX.Element, options?: MountOptions<HooksConfig>) => Promise<MountResult>; interface AppHooksConfig {
router: Partial<Pick<typeof router, 'query' | 'isReady' | 'asPath' | 'pathname'>>;
interface Options extends JsonObject {
hooksConfig?: {
router: Partial<Pick<typeof router, 'query' | 'isReady' | 'asPath' | 'pathname'>>;
};
} }
export type RenderFixture = (component: JSX.Element, options?: Options, props?: Omit<TestAppProps, 'children'>) => Promise<MountResult> type Mount = <HooksConfig extends AppHooksConfig>(component: JSX.Element, options?: MountOptions<HooksConfig>) => Promise<MountResult>
export type RenderFixture = (component: JSX.Element, options?: MountOptions<AppHooksConfig>, props?: Omit<TestAppProps, 'children'>) => Promise<MountResult>
const fixture: TestFixture<RenderFixture, { mount: Mount }> = async({ mount }, use) => { const fixture: TestFixture<RenderFixture, { mount: Mount }> = async({ mount }, use) => {
await use((component, options, props) => { await use((component, options, props) => {
......
/* eslint-disable no-console */ /* eslint-disable no-console */
import { test as base } from '@playwright/experimental-ct-react'; import { test as base } from '@playwright/experimental-ct-react';
import type { Page } from '@playwright/test';
import * as injectMetaMaskProvider from './fixtures/injectMetaMaskProvider'; import * as injectMetaMaskProvider from './fixtures/injectMetaMaskProvider';
import * as mockApiResponse from './fixtures/mockApiResponse'; import * as mockApiResponse from './fixtures/mockApiResponse';
...@@ -13,7 +14,7 @@ import * as mockTextAd from './fixtures/mockTextAd'; ...@@ -13,7 +14,7 @@ import * as mockTextAd from './fixtures/mockTextAd';
import * as render from './fixtures/render'; import * as render from './fixtures/render';
import * as socketServer from './fixtures/socketServer'; import * as socketServer from './fixtures/socketServer';
interface Fixtures { export interface Fixtures {
render: render.RenderFixture; render: render.RenderFixture;
mockApiResponse: mockApiResponse.MockApiResponseFixture; mockApiResponse: mockApiResponse.MockApiResponseFixture;
mockAssetResponse: mockAssetResponse.MockAssetResponseFixture; mockAssetResponse: mockAssetResponse.MockAssetResponseFixture;
...@@ -27,6 +28,8 @@ interface Fixtures { ...@@ -27,6 +28,8 @@ interface Fixtures {
mockTextAd: mockTextAd.MockTextAdFixture; mockTextAd: mockTextAd.MockTextAdFixture;
} }
export type TestFnArgs = Fixtures & { page: Page };
const test = base.extend<Fixtures>({ const test = base.extend<Fixtures>({
render: render.default, render: render.default,
mockApiResponse: mockApiResponse.default, mockApiResponse: mockApiResponse.default,
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
export type IconName = export type IconName =
| "ABI_slim" | "ABI_slim"
| "ABI" | "ABI"
| "API_slim"
| "API" | "API"
| "apps_list" | "apps_list"
| "apps_slim" | "apps_slim"
...@@ -47,7 +48,6 @@ ...@@ -47,7 +48,6 @@
| "donate" | "donate"
| "dots" | "dots"
| "edit" | "edit"
| "email-sent"
| "email" | "email"
| "empty_search_result" | "empty_search_result"
| "ENS_slim" | "ENS_slim"
...@@ -106,6 +106,7 @@ ...@@ -106,6 +106,7 @@
| "output_roots" | "output_roots"
| "payment_link" | "payment_link"
| "plus" | "plus"
| "private_tags_slim"
| "privattags" | "privattags"
| "profile" | "profile"
| "publictags_slim" | "publictags_slim"
...@@ -122,6 +123,7 @@ ...@@ -122,6 +123,7 @@
| "score/score-ok" | "score/score-ok"
| "search" | "search"
| "share" | "share"
| "sign_out"
| "social/canny" | "social/canny"
| "social/coingecko" | "social/coingecko"
| "social/coinmarketcap" | "social/coinmarketcap"
...@@ -166,6 +168,7 @@ ...@@ -166,6 +168,7 @@
| "validator" | "validator"
| "verification-steps/finalized" | "verification-steps/finalized"
| "verification-steps/unfinalized" | "verification-steps/unfinalized"
| "verified_slim"
| "verified" | "verified"
| "wallet" | "wallet"
| "wallets/coinbase" | "wallets/coinbase"
......
...@@ -51,24 +51,28 @@ export const STATS_CHARTS_SECTION: stats.LineChartSection = { ...@@ -51,24 +51,28 @@ export const STATS_CHARTS_SECTION: stats.LineChartSection = {
title: 'Average transaction fee', title: 'Average transaction fee',
description: 'The average amount in ETH spent per transaction', description: 'The average amount in ETH spent per transaction',
units: 'ETH', units: 'ETH',
resolutions: [ 'DAY', 'MONTH' ],
}, },
{ {
id: 'chart_1', id: 'chart_1',
title: 'Transactions fees', title: 'Transactions fees',
description: 'Amount of tokens paid as fees', description: 'Amount of tokens paid as fees',
units: 'ETH', units: 'ETH',
resolutions: [ 'DAY', 'MONTH' ],
}, },
{ {
id: 'chart_2', id: 'chart_2',
title: 'New transactions', title: 'New transactions',
description: 'New transactions number', description: 'New transactions number',
units: undefined, units: undefined,
resolutions: [ 'DAY', 'MONTH' ],
}, },
{ {
id: 'chart_3', id: 'chart_3',
title: 'Transactions growth', title: 'Transactions growth',
description: 'Cumulative transactions number', description: 'Cumulative transactions number',
units: undefined, units: undefined,
resolutions: [ 'DAY', 'MONTH' ],
}, },
], ],
}; };
......
import type { Validator, ValidatorsCountersResponse } from 'types/api/validators'; import type {
ValidatorStability,
ValidatorsStabilityCountersResponse,
ValidatorBlackfort,
ValidatorsBlackfortCountersResponse,
} from 'types/api/validators';
import { ADDRESS_PARAMS } from './addressParams'; import { ADDRESS_PARAMS } from './addressParams';
export const VALIDATOR: Validator = { export const VALIDATOR_STABILITY: ValidatorStability = {
address: ADDRESS_PARAMS, address: ADDRESS_PARAMS,
blocks_validated_count: 25987, blocks_validated_count: 25987,
state: 'active', state: 'active',
}; };
export const VALIDATORS_COUNTERS: ValidatorsCountersResponse = { export const VALIDATORS_STABILITY_COUNTERS: ValidatorsStabilityCountersResponse = {
active_validators_counter: '42', active_validators_counter: '42',
active_validators_percentage: 7.14, active_validators_percentage: 7.14,
new_validators_counter_24h: '11', new_validators_counter_24h: '11',
validators_counter: '140', validators_counter: '140',
}; };
export const VALIDATOR_BLACKFORT: ValidatorBlackfort = {
address: ADDRESS_PARAMS,
name: 'testnet-1',
commission: 10,
delegated_amount: '0',
self_bonded_amount: '10000',
};
export const VALIDATORS_BLACKFORT_COUNTERS: ValidatorsBlackfortCountersResponse = {
new_validators_counter_24h: '11',
validators_counter: '140',
};
import { defineStyle, defineStyleConfig } from '@chakra-ui/styled-system';
import getOutlinedFieldStyles from '../utils/getOutlinedFieldStyles';
const baseStyle = defineStyle({
textAlign: 'center',
bgColor: 'dialog_bg',
});
const sizes = {
md: defineStyle({
fontSize: 'md',
w: 10,
h: 10,
borderRadius: 'md',
}),
};
const variants = {
outline: defineStyle(
(props) => getOutlinedFieldStyles(props),
),
};
const PinInput = defineStyleConfig({
baseStyle,
sizes,
variants,
defaultProps: {
size: 'md',
},
});
export default PinInput;
...@@ -47,6 +47,16 @@ const sizes = { ...@@ -47,6 +47,16 @@ const sizes = {
lineHeight: 5, lineHeight: 5,
}, },
}), }),
md: definePartsStyle({
container: {
minH: 8,
minW: 8,
fontSize: 'sm',
px: '6px',
py: '6px',
lineHeight: 5,
},
}),
}; };
const baseStyleContainer = defineStyle({ const baseStyleContainer = defineStyle({
......
...@@ -10,6 +10,7 @@ import Input from './Input'; ...@@ -10,6 +10,7 @@ import Input from './Input';
import Link from './Link'; import Link from './Link';
import Menu from './Menu'; import Menu from './Menu';
import Modal from './Modal'; import Modal from './Modal';
import PinInput from './PinInput';
import Popover from './Popover'; import Popover from './Popover';
import Radio from './Radio'; import Radio from './Radio';
import Select from './Select'; import Select from './Select';
...@@ -36,6 +37,7 @@ const components = { ...@@ -36,6 +37,7 @@ const components = {
Link, Link,
Menu, Menu,
Modal, Modal,
PinInput,
Popover, Popover,
Radio, Radio,
Select, Select,
......
...@@ -3,6 +3,7 @@ import { mode } from '@chakra-ui/theme-tools'; ...@@ -3,6 +3,7 @@ import { mode } from '@chakra-ui/theme-tools';
import scrollbar from './foundations/scrollbar'; import scrollbar from './foundations/scrollbar';
import addressEntity from './globals/address-entity'; import addressEntity from './globals/address-entity';
import recaptcha from './globals/recaptcha';
import getDefaultTransitionProps from './utils/getDefaultTransitionProps'; import getDefaultTransitionProps from './utils/getDefaultTransitionProps';
const global = (props: StyleFunctionProps) => ({ const global = (props: StyleFunctionProps) => ({
...@@ -25,6 +26,7 @@ const global = (props: StyleFunctionProps) => ({ ...@@ -25,6 +26,7 @@ const global = (props: StyleFunctionProps) => ({
}, },
...scrollbar(props), ...scrollbar(props),
...addressEntity(props), ...addressEntity(props),
...recaptcha(),
}); });
export default global; export default global;
const styles = () => {
return {
'.grecaptcha-badge': {
zIndex: 'toast',
},
};
};
export default styles;
...@@ -5,6 +5,7 @@ import path from 'path'; ...@@ -5,6 +5,7 @@ import path from 'path';
const PRESETS = { const PRESETS = {
arbitrum: 'https://arbitrum.blockscout.com', arbitrum: 'https://arbitrum.blockscout.com',
base: 'https://base.blockscout.com', base: 'https://base.blockscout.com',
blackfort_testnet: 'https://blackfort-testnet.blockscout.com',
celo_alfajores: 'https://celo-alfajores.blockscout.com', celo_alfajores: 'https://celo-alfajores.blockscout.com',
eth: 'https://eth.blockscout.com', eth: 'https://eth.blockscout.com',
eth_goerli: 'https://eth-goerli.blockscout.com', eth_goerli: 'https://eth-goerli.blockscout.com',
...@@ -18,6 +19,7 @@ const PRESETS = { ...@@ -18,6 +19,7 @@ const PRESETS = {
stability_testnet: 'https://stability-testnet.blockscout.com', stability_testnet: 'https://stability-testnet.blockscout.com',
zkevm: 'https://zkevm.blockscout.com', zkevm: 'https://zkevm.blockscout.com',
zksync: 'https://zksync.blockscout.com', zksync: 'https://zksync.blockscout.com',
zora: 'https://explorer.zora.energy',
// main === staging // main === staging
main: 'https://eth-sepolia.k8s-dev.blockscout.com', main: 'https://eth-sepolia.k8s-dev.blockscout.com',
}; };
......
...@@ -71,6 +71,7 @@ export interface UserInfo { ...@@ -71,6 +71,7 @@ export interface UserInfo {
name?: string; name?: string;
nickname?: string; nickname?: string;
email: string | null; email: string | null;
address_hash: string | null;
avatar?: string; avatar?: string;
} }
......
import type { Block } from './block'; import type { Block } from './block';
import type { Transaction } from './transaction'; import type { Transaction } from './transaction';
export interface ArbitrumLatestDepositsItem {
completion_transaction_hash: string;
origination_timestamp: string | null;
origination_transaction_block_number: number | null;
origination_transaction_hash: string | null;
}
export interface ArbitrumLatestDepositsResponse {
items: Array<ArbitrumLatestDepositsItem>;
}
export type ArbitrumL2MessagesItem = { export type ArbitrumL2MessagesItem = {
completion_transaction_hash: string | null; completion_transaction_hash: string | null;
id: number; id: number;
origination_address: string; origination_address: string;
origination_timestamp: string | null; origination_timestamp: string | null;
origination_transaction_block_number: number; origination_transaction_block_number: number | null;
origination_transaction_hash: string; origination_transaction_hash: string;
status: 'initiated' | 'sent' | 'confirmed' | 'relayed'; status: 'initiated' | 'sent' | 'confirmed' | 'relayed';
} }
......
import type { AddressParam } from './addressParams'; import type { AddressParam } from './addressParams';
export interface Validator { export interface ValidatorStability {
address: AddressParam; address: AddressParam;
blocks_validated_count: number; blocks_validated_count: number;
state: 'active' | 'probation' | 'inactive'; state: 'active' | 'probation' | 'inactive';
} }
export interface ValidatorsResponse { export interface ValidatorsStabilityResponse {
items: Array<Validator>; items: Array<ValidatorStability>;
next_page_params: { next_page_params: {
'address_hash': string; 'address_hash': string;
'blocks_validated': string; 'blocks_validated': string;
'items_count': string; 'items_count': string;
'state': Validator['state']; 'state': ValidatorStability['state'];
} | null; } | null;
} }
export interface ValidatorsCountersResponse { export interface ValidatorsStabilityCountersResponse {
active_validators_counter: string; active_validators_counter: string;
active_validators_percentage: number; active_validators_percentage: number;
new_validators_counter_24h: string; new_validators_counter_24h: string;
validators_counter: string; validators_counter: string;
} }
export interface ValidatorsFilters { export interface ValidatorsStabilityFilters {
// address_hash: string | undefined; // right now API doesn't support filtering by address_hash // address_hash: string | undefined; // right now API doesn't support filtering by address_hash
state_filter: Validator['state'] | undefined; state_filter: ValidatorStability['state'] | undefined;
} }
export interface ValidatorsSorting { export interface ValidatorsStabilitySorting {
sort: 'state' | 'blocks_validated'; sort: 'state' | 'blocks_validated';
order: 'asc' | 'desc'; order: 'asc' | 'desc';
} }
export type ValidatorsSortingField = ValidatorsSorting['sort']; export type ValidatorsStabilitySortingField = ValidatorsStabilitySorting['sort'];
export type ValidatorsSortingValue = `${ ValidatorsSortingField }-${ ValidatorsSorting['order'] }`; export type ValidatorsStabilitySortingValue = `${ ValidatorsStabilitySortingField }-${ ValidatorsStabilitySorting['order'] }`;
export interface ValidatorBlackfort {
address: AddressParam;
name: string;
commission: number;
delegated_amount: string;
self_bonded_amount: string;
}
export interface ValidatorsBlackfortResponse {
items: Array<ValidatorBlackfort>;
next_page_params: {
'address_hash': string;
} | null;
}
export interface ValidatorsBlackfortCountersResponse {
new_validators_counter_24h: string;
validators_counter: string;
}
export interface ValidatorsBlackfortSorting {
sort: 'address_hash';
order: 'asc' | 'desc';
}
export type ValidatorsBlackfortSortingField = ValidatorsBlackfortSorting['sort'];
export type ValidatorsBlackfortSortingValue = `${ ValidatorsBlackfortSortingField }-${ ValidatorsBlackfortSorting['order'] }`;
export type AddressProfileAPIConfig = {
api_url_template: string;
tag_link_template?: string;
tag_icon?: string;
tag_bg_color?: string;
tag_text_color?: string;
};
...@@ -2,6 +2,7 @@ import type { ArrayElement } from 'types/utils'; ...@@ -2,6 +2,7 @@ import type { ArrayElement } from 'types/utils';
export const VALIDATORS_CHAIN_TYPE = [ export const VALIDATORS_CHAIN_TYPE = [
'stability', 'stability',
'blackfort',
] as const; ] as const;
export type ValidatorsChainType = ArrayElement<typeof VALIDATORS_CHAIN_TYPE>; export type ValidatorsChainType = ArrayElement<typeof VALIDATORS_CHAIN_TYPE>;
import React from 'react'; import React from 'react';
import * as balanceHistoryMock from 'mocks/address/coinBalanceHistory'; import * as balanceHistoryMock from 'mocks/address/coinBalanceHistory';
import { test, expect } from 'playwright/lib'; import { test, expect, devices } from 'playwright/lib';
import AddressCoinBalance from './AddressCoinBalance'; import AddressCoinBalance from './AddressCoinBalance';
...@@ -12,7 +12,7 @@ const hooksConfig = { ...@@ -12,7 +12,7 @@ const hooksConfig = {
}, },
}; };
test('base view +@dark-mode +@mobile', async({ render, page, mockApiResponse }) => { test('base view +@dark-mode', async({ render, page, mockApiResponse }) => {
await mockApiResponse('address_coin_balance', balanceHistoryMock.baseResponse, { pathParams: { hash: addressHash } }); await mockApiResponse('address_coin_balance', balanceHistoryMock.baseResponse, { pathParams: { hash: addressHash } });
await mockApiResponse('address_coin_balance_chart', balanceHistoryMock.chartResponse, { pathParams: { hash: addressHash } }); await mockApiResponse('address_coin_balance_chart', balanceHistoryMock.chartResponse, { pathParams: { hash: addressHash } });
const component = await render(<AddressCoinBalance/>, { hooksConfig }); const component = await render(<AddressCoinBalance/>, { hooksConfig });
...@@ -23,3 +23,19 @@ test('base view +@dark-mode +@mobile', async({ render, page, mockApiResponse }) ...@@ -23,3 +23,19 @@ test('base view +@dark-mode +@mobile', async({ render, page, mockApiResponse })
await page.mouse.move(240, 100); await page.mouse.move(240, 100);
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
test.describe('mobile', () => {
test.use({ viewport: devices['iPhone 13 Pro'].viewport });
test('base view', async({ render, page, mockApiResponse }) => {
await mockApiResponse('address_coin_balance', balanceHistoryMock.baseResponse, { pathParams: { hash: addressHash } });
await mockApiResponse('address_coin_balance_chart', balanceHistoryMock.chartResponse, { pathParams: { hash: addressHash } });
const component = await render(<AddressCoinBalance/>, { hooksConfig });
await page.waitForFunction(() => {
return document.querySelector('path[data-name="chart-Balances-small"]')?.getAttribute('opacity') === '1';
});
await page.mouse.move(100, 100);
await page.mouse.move(240, 100);
await expect(component).toHaveScreenshot();
});
});
...@@ -3,31 +3,31 @@ import React from 'react'; ...@@ -3,31 +3,31 @@ import React from 'react';
import config from 'configs/app'; import config from 'configs/app';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import useWeb3Wallet from 'lib/web3/useWallet';
import AddressEntity from 'ui/shared/entities/address/AddressEntity'; import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import useWallet from 'ui/snippets/walletMenu/useWallet';
interface Props { interface Props {
isLoading?: boolean; isLoading?: boolean;
} }
const ContractConnectWallet = ({ isLoading }: Props) => { const ContractConnectWallet = ({ isLoading }: Props) => {
const { isModalOpening, isModalOpen, connect, disconnect, address, isWalletConnected } = useWallet({ source: 'Smart contracts' }); const web3Wallet = useWeb3Wallet({ source: 'Smart contracts' });
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const content = (() => { const content = (() => {
if (!isWalletConnected) { if (!web3Wallet.isConnected) {
return ( return (
<> <>
<span>Disconnected</span> <span>Disconnected</span>
<Button <Button
ml={ 3 } ml={ 3 }
onClick={ connect } onClick={ web3Wallet.connect }
size="sm" size="sm"
variant="outline" variant="outline"
isLoading={ isModalOpening || isModalOpen } isLoading={ web3Wallet.isOpen }
loadingText="Connect wallet" loadingText="Connect wallet"
> >
Connect wallet Connect wallet
</Button> </Button>
</> </>
); );
...@@ -38,20 +38,20 @@ const ContractConnectWallet = ({ isLoading }: Props) => { ...@@ -38,20 +38,20 @@ const ContractConnectWallet = ({ isLoading }: Props) => {
<Flex alignItems="center"> <Flex alignItems="center">
<span>Connected to </span> <span>Connected to </span>
<AddressEntity <AddressEntity
address={{ hash: address }} address={{ hash: web3Wallet.address || '' }}
truncation={ isMobile ? 'constant' : 'dynamic' } truncation={ isMobile ? 'constant' : 'dynamic' }
fontWeight={ 600 } fontWeight={ 600 }
ml={ 2 } ml={ 2 }
/> />
</Flex> </Flex>
<Button onClick={ disconnect } size="sm" variant="outline">Disconnect</Button> <Button onClick={ web3Wallet.disconnect } size="sm" variant="outline">Disconnect</Button>
</Flex> </Flex>
); );
})(); })();
return ( return (
<Skeleton isLoaded={ !isLoading } mb={ 6 }> <Skeleton isLoaded={ !isLoading } mb={ 6 }>
<Alert status={ address ? 'success' : 'warning' }> <Alert status={ web3Wallet.address ? 'success' : 'warning' }>
{ content } { content }
</Alert> </Alert>
</Skeleton> </Skeleton>
......
...@@ -42,6 +42,7 @@ const ContractMethodFieldInputTuple = ({ data, basePath, level, isDisabled, isOp ...@@ -42,6 +42,7 @@ const ContractMethodFieldInputTuple = ({ data, basePath, level, isDisabled, isOp
basePath={ `${ basePath }:${ index }` } basePath={ `${ basePath }:${ index }` }
level={ level + 1 } level={ level + 1 }
isDisabled={ isDisabled } isDisabled={ isDisabled }
isOptional={ isOptional }
/> />
); );
} }
......
...@@ -74,4 +74,27 @@ describe('transformFormDataToMethodArgs', () => { ...@@ -74,4 +74,27 @@ describe('transformFormDataToMethodArgs', () => {
], ],
]); ]);
}); });
it('should transform all nested empty arrays to empty arrays', () => {
const formData = {
'0': '0x1D415D28380ff51A507F7B176ca5F27833F7FffD',
'1': '0x1D415D28380ff51A507F7B176ca5F27833F7FffD',
'2': '3160',
'3': true,
// tuple array without elements
'4:0:0:0': undefined,
'4:0:1:0': undefined,
'4:0:1:1': undefined,
'4:0:1:2': undefined,
'4:0:1:3': undefined,
};
const result = transformFormDataToMethodArgs(formData);
expect(result).toEqual([
'0x1D415D28380ff51A507F7B176ca5F27833F7FffD',
'0x1D415D28380ff51A507F7B176ca5F27833F7FffD',
'3160',
true,
[],
]);
});
}); });
...@@ -81,7 +81,9 @@ export function transformFormDataToMethodArgs(formData: ContractMethodFormFields ...@@ -81,7 +81,9 @@ export function transformFormDataToMethodArgs(formData: ContractMethodFormFields
_set(result, field.replaceAll(':', '.'), value); _set(result, field.replaceAll(':', '.'), value);
} }
return filterOutEmptyItems(result); const filteredResult = filterOutEmptyItems(result);
const mappedResult = mapEmptyNestedArrays(filteredResult);
return mappedResult;
} }
function filterOutEmptyItems(array: Array<unknown>): Array<unknown> { function filterOutEmptyItems(array: Array<unknown>): Array<unknown> {
...@@ -90,11 +92,26 @@ function filterOutEmptyItems(array: Array<unknown>): Array<unknown> { ...@@ -90,11 +92,26 @@ function filterOutEmptyItems(array: Array<unknown>): Array<unknown> {
// The only optional field is the native coin value, which is safely handled in the form submit handler. // The only optional field is the native coin value, which is safely handled in the form submit handler.
// 2. When the user adds and removes items from a field array. // 2. When the user adds and removes items from a field array.
// In this scenario, empty items need to be filtered out to maintain the correct sequence of arguments. // In this scenario, empty items need to be filtered out to maintain the correct sequence of arguments.
// We don't use isEmptyField() function here because of the second case otherwise it will not keep the correct order of arguments.
return array return array
.map((item) => Array.isArray(item) ? filterOutEmptyItems(item) : item) .map((item) => Array.isArray(item) ? filterOutEmptyItems(item) : item)
.filter((item) => item !== undefined); .filter((item) => item !== undefined);
} }
function isEmptyField(field: unknown): boolean {
// the empty string is meant that the field was touched but left empty
// the undefined is meant that the field was not touched
return field === undefined || field === '';
}
function isEmptyNestedArray(array: Array<unknown>): boolean {
return array.flat(Infinity).filter((item) => !isEmptyField(item)).length === 0;
}
function mapEmptyNestedArrays(array: Array<unknown>): Array<unknown> {
return array.map((item) => Array.isArray(item) && isEmptyNestedArray(item) ? [] : item);
}
export function getFieldLabel(input: ContractAbiItemInput, isRequired?: boolean) { export function getFieldLabel(input: ContractAbiItemInput, isRequired?: boolean) {
const name = input.name || input.internalType || '<unnamed argument>'; const name = input.name || input.internalType || '<unnamed argument>';
return `${ name } (${ input.type })${ isRequired ? '*' : '' }`; return `${ name } (${ input.type })${ isRequired ? '*' : '' }`;
......
...@@ -28,11 +28,14 @@ export default function useCallMethodPublicClient(): (params: Params) => Promise ...@@ -28,11 +28,14 @@ export default function useCallMethodPublicClient(): (params: Params) => Promise
} }
const address = getAddress(addressHash); const address = getAddress(addressHash);
// for write payable methods we add additional input for native coin value
// so in simulate mode we need to strip it off
const _args = args.slice(0, item.inputs.length);
const params = { const params = {
abi: [ item ], abi: [ item ],
functionName: item.name, functionName: item.name,
args: args, args: _args,
address, address,
account, account,
}; };
......
...@@ -5,10 +5,10 @@ import React from 'react'; ...@@ -5,10 +5,10 @@ import React from 'react';
import config from 'configs/app'; import config from 'configs/app';
import { getResourceKey } from 'lib/api/useApiQuery'; import { getResourceKey } from 'lib/api/useApiQuery';
import useIsAccountActionAllowed from 'lib/hooks/useIsAccountActionAllowed';
import usePreventFocusAfterModalClosing from 'lib/hooks/usePreventFocusAfterModalClosing'; import usePreventFocusAfterModalClosing from 'lib/hooks/usePreventFocusAfterModalClosing';
import * as mixpanel from 'lib/mixpanel/index'; import * as mixpanel from 'lib/mixpanel/index';
import IconSvg from 'ui/shared/IconSvg'; import IconSvg from 'ui/shared/IconSvg';
import AuthGuard from 'ui/snippets/auth/AuthGuard';
import WatchlistAddModal from 'ui/watchlist/AddressModal/AddressModal'; import WatchlistAddModal from 'ui/watchlist/AddressModal/AddressModal';
import DeleteAddressModal from 'ui/watchlist/DeleteAddressModal'; import DeleteAddressModal from 'ui/watchlist/DeleteAddressModal';
...@@ -23,16 +23,12 @@ const AddressFavoriteButton = ({ className, hash, watchListId }: Props) => { ...@@ -23,16 +23,12 @@ const AddressFavoriteButton = ({ className, hash, watchListId }: Props) => {
const deleteModalProps = useDisclosure(); const deleteModalProps = useDisclosure();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const router = useRouter(); const router = useRouter();
const isAccountActionAllowed = useIsAccountActionAllowed();
const onFocusCapture = usePreventFocusAfterModalClosing(); const onFocusCapture = usePreventFocusAfterModalClosing();
const handleClick = React.useCallback(() => { const handleAddToFavorite = React.useCallback(() => {
if (!isAccountActionAllowed()) {
return;
}
watchListId ? deleteModalProps.onOpen() : addModalProps.onOpen(); watchListId ? deleteModalProps.onOpen() : addModalProps.onOpen();
!watchListId && mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'Add to watchlist' }); !watchListId && mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'Add to watchlist' });
}, [ isAccountActionAllowed, watchListId, deleteModalProps, addModalProps ]); }, [ watchListId, deleteModalProps, addModalProps ]);
const handleAddOrDeleteSuccess = React.useCallback(async() => { const handleAddOrDeleteSuccess = React.useCallback(async() => {
const queryKey = getResourceKey('address', { pathParams: { hash: router.query.hash?.toString() } }); const queryKey = getResourceKey('address', { pathParams: { hash: router.query.hash?.toString() } });
...@@ -50,7 +46,7 @@ const AddressFavoriteButton = ({ className, hash, watchListId }: Props) => { ...@@ -50,7 +46,7 @@ const AddressFavoriteButton = ({ className, hash, watchListId }: Props) => {
const formData = React.useMemo(() => { const formData = React.useMemo(() => {
if (typeof watchListId !== 'number') { if (typeof watchListId !== 'number') {
return; return { address_hash: hash };
} }
return { return {
...@@ -65,21 +61,25 @@ const AddressFavoriteButton = ({ className, hash, watchListId }: Props) => { ...@@ -65,21 +61,25 @@ const AddressFavoriteButton = ({ className, hash, watchListId }: Props) => {
return ( return (
<> <>
<Tooltip label={ `${ watchListId ? 'Remove address from Watch list' : 'Add address to Watch list' }` }> <AuthGuard onAuthSuccess={ handleAddToFavorite }>
<IconButton { ({ onClick }) => (
isActive={ Boolean(watchListId) } <Tooltip label={ `${ watchListId ? 'Remove address from Watch list' : 'Add address to Watch list' }` }>
className={ className } <IconButton
aria-label="edit" isActive={ Boolean(watchListId) }
variant="outline" className={ className }
size="sm" aria-label="edit"
pl="6px" variant="outline"
pr="6px" size="sm"
flexShrink={ 0 } pl="6px"
onClick={ handleClick } pr="6px"
icon={ <IconSvg name={ watchListId ? 'star_filled' : 'star_outline' } boxSize={ 5 }/> } flexShrink={ 0 }
onFocusCapture={ onFocusCapture } onClick={ onClick }
/> icon={ <IconSvg name={ watchListId ? 'star_filled' : 'star_outline' } boxSize={ 5 }/> }
</Tooltip> onFocusCapture={ onFocusCapture }
/>
</Tooltip>
) }
</AuthGuard>
<WatchlistAddModal <WatchlistAddModal
{ ...addModalProps } { ...addModalProps }
isAdd isAdd
...@@ -87,7 +87,7 @@ const AddressFavoriteButton = ({ className, hash, watchListId }: Props) => { ...@@ -87,7 +87,7 @@ const AddressFavoriteButton = ({ className, hash, watchListId }: Props) => {
onSuccess={ handleAddOrDeleteSuccess } onSuccess={ handleAddOrDeleteSuccess }
data={ formData } data={ formData }
/> />
{ formData && ( { formData.id && (
<DeleteAddressModal <DeleteAddressModal
{ ...deleteModalProps } { ...deleteModalProps }
onClose={ handleDeleteModalClose } onClose={ handleDeleteModalClose }
......
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.
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.
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.
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