Commit 6f19c4a1 authored by tom's avatar tom

Merge branch 'main' of github.com:blockscout/frontend into release/v1-37-5

parents e5829cf5 c9606a18
...@@ -17,6 +17,9 @@ ...@@ -17,6 +17,9 @@
/public/icons/sprite.svg /public/icons/sprite.svg
/public/icons/sprite.*.svg /public/icons/sprite.*.svg
/public/icons/README.md /public/icons/README.md
/public/static/og_image.png
/public/sitemap.xml
/public/robots.txt
/analyze /analyze
# production # production
......
...@@ -33,6 +33,12 @@ WORKDIR /favicon-generator ...@@ -33,6 +33,12 @@ WORKDIR /favicon-generator
COPY ./deploy/tools/favicon-generator/package.json ./deploy/tools/favicon-generator/yarn.lock ./ COPY ./deploy/tools/favicon-generator/package.json ./deploy/tools/favicon-generator/yarn.lock ./
RUN yarn --frozen-lockfile --network-timeout 100000 RUN yarn --frozen-lockfile --network-timeout 100000
### SITEMAP GENERATOR
# Install dependencies
WORKDIR /sitemap-generator
COPY ./deploy/tools/sitemap-generator/package.json ./deploy/tools/sitemap-generator/yarn.lock ./
RUN yarn --frozen-lockfile --network-timeout 100000
# ***************************** # *****************************
# ****** STAGE 2: Build ******* # ****** STAGE 2: Build *******
...@@ -88,6 +94,12 @@ RUN cd ./deploy/tools/envs-validator && yarn build ...@@ -88,6 +94,12 @@ RUN cd ./deploy/tools/envs-validator && yarn build
# Copy dependencies and source code # Copy dependencies and source code
COPY --from=deps /favicon-generator/node_modules ./deploy/tools/favicon-generator/node_modules COPY --from=deps /favicon-generator/node_modules ./deploy/tools/favicon-generator/node_modules
### SITEMAP GENERATOR
# Copy dependencies and source code
COPY --from=deps /sitemap-generator/node_modules ./deploy/tools/sitemap-generator/node_modules
# ***************************** # *****************************
# ******* STAGE 3: Run ******** # ******* STAGE 3: Run ********
# ***************************** # *****************************
...@@ -122,11 +134,16 @@ COPY --chmod=755 ./deploy/scripts/validate_envs.sh . ...@@ -122,11 +134,16 @@ COPY --chmod=755 ./deploy/scripts/validate_envs.sh .
COPY --chmod=755 ./deploy/scripts/make_envs_script.sh . COPY --chmod=755 ./deploy/scripts/make_envs_script.sh .
## Assets downloader ## Assets downloader
COPY --chmod=755 ./deploy/scripts/download_assets.sh . COPY --chmod=755 ./deploy/scripts/download_assets.sh .
## OG image generator
COPY ./deploy/scripts/og_image_generator.js .
## Favicon generator ## Favicon generator
COPY --chmod=755 ./deploy/scripts/favicon_generator.sh . COPY --chmod=755 ./deploy/scripts/favicon_generator.sh .
COPY --from=builder /app/deploy/tools/favicon-generator ./deploy/tools/favicon-generator COPY --from=builder /app/deploy/tools/favicon-generator ./deploy/tools/favicon-generator
RUN ["chmod", "-R", "777", "./deploy/tools/favicon-generator"] RUN ["chmod", "-R", "777", "./deploy/tools/favicon-generator"]
RUN ["chmod", "-R", "777", "./public"] RUN ["chmod", "-R", "777", "./public"]
## Sitemap generator
COPY --chmod=755 ./deploy/scripts/sitemap_generator.sh .
COPY --from=builder /app/deploy/tools/sitemap-generator ./deploy/tools/sitemap-generator
# Copy ENVs files # Copy ENVs files
COPY --from=builder /app/.env.registry . COPY --from=builder /app/.env.registry .
......
import type { RollupType } from 'types/client/rollup'; import type { RollupType } from 'types/client/rollup';
import type { NetworkVerificationType, NetworkVerificationTypeEnvs } from 'types/networks'; import type { NetworkVerificationType, NetworkVerificationTypeEnvs } from 'types/networks';
import { getEnvValue } from './utils'; import { urlValidator } from 'ui/shared/forms/validators/url';
import { getEnvValue, parseEnvJson } from './utils';
const DEFAULT_CURRENCY_DECIMALS = 18; const DEFAULT_CURRENCY_DECIMALS = 18;
...@@ -17,6 +19,19 @@ const verificationType: NetworkVerificationType = (() => { ...@@ -17,6 +19,19 @@ const verificationType: NetworkVerificationType = (() => {
return getEnvValue('NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE') as NetworkVerificationTypeEnvs || 'mining'; return getEnvValue('NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE') as NetworkVerificationTypeEnvs || 'mining';
})(); })();
const rpcUrls = (() => {
const envValue = getEnvValue('NEXT_PUBLIC_NETWORK_RPC_URL');
const isUrl = urlValidator(envValue);
if (envValue && isUrl === true) {
return [ envValue ];
}
const parsedValue = parseEnvJson<Array<string>>(envValue);
return Array.isArray(parsedValue) ? parsedValue : [];
})();
const chain = Object.freeze({ const chain = Object.freeze({
id: getEnvValue('NEXT_PUBLIC_NETWORK_ID'), id: getEnvValue('NEXT_PUBLIC_NETWORK_ID'),
name: getEnvValue('NEXT_PUBLIC_NETWORK_NAME'), name: getEnvValue('NEXT_PUBLIC_NETWORK_NAME'),
...@@ -32,7 +47,7 @@ const chain = Object.freeze({ ...@@ -32,7 +47,7 @@ const chain = Object.freeze({
}, },
hasMultipleGasCurrencies: getEnvValue('NEXT_PUBLIC_NETWORK_MULTIPLE_GAS_CURRENCIES') === 'true', hasMultipleGasCurrencies: getEnvValue('NEXT_PUBLIC_NETWORK_MULTIPLE_GAS_CURRENCIES') === 'true',
tokenStandard: getEnvValue('NEXT_PUBLIC_NETWORK_TOKEN_STANDARD_NAME') || 'ERC', tokenStandard: getEnvValue('NEXT_PUBLIC_NETWORK_TOKEN_STANDARD_NAME') || 'ERC',
rpcUrl: getEnvValue('NEXT_PUBLIC_NETWORK_RPC_URL'), rpcUrls,
isTestnet: getEnvValue('NEXT_PUBLIC_IS_TESTNET') === 'true', isTestnet: getEnvValue('NEXT_PUBLIC_IS_TESTNET') === 'true',
verificationType, verificationType,
}); });
......
...@@ -17,7 +17,7 @@ const config: Feature<{ walletConnect: { projectId: string } }> = (() => { ...@@ -17,7 +17,7 @@ const config: Feature<{ walletConnect: { projectId: string } }> = (() => {
chain.currency.name && chain.currency.name &&
chain.currency.symbol && chain.currency.symbol &&
chain.currency.decimals && chain.currency.decimals &&
chain.rpcUrl && chain.rpcUrls.length > 0 &&
walletConnectProjectId walletConnectProjectId
) { ) {
return Object.freeze({ return Object.freeze({
......
...@@ -33,7 +33,7 @@ const config: Feature<( ...@@ -33,7 +33,7 @@ const config: Feature<(
rating: { airtableApiKey: string; airtableBaseId: string } | undefined; rating: { airtableApiKey: string; airtableBaseId: string } | undefined;
graphLinksUrl: string | undefined; graphLinksUrl: string | undefined;
}> = (() => { }> = (() => {
if (enabled === 'true' && chain.rpcUrl && submitFormUrl) { if (enabled === 'true' && chain.rpcUrls.length > 0 && submitFormUrl) {
const props = { const props = {
submitFormUrl, submitFormUrl,
categoriesUrl, categoriesUrl,
......
...@@ -31,7 +31,7 @@ const config: Feature<{ ...@@ -31,7 +31,7 @@ const config: Feature<{
type, type,
L1BaseUrl: stripTrailingSlash(L1BaseUrl), L1BaseUrl: stripTrailingSlash(L1BaseUrl),
L2WithdrawalUrl: type === 'optimistic' ? L2WithdrawalUrl : undefined, L2WithdrawalUrl: type === 'optimistic' ? L2WithdrawalUrl : undefined,
outputRootsEnabled: type === 'optimistic' && getEnvValue('NEXT_PUBLIC_ROLLUP_OUTPUT_ROOTS_ENABLED') !== 'false', outputRootsEnabled: type === 'optimistic' && getEnvValue('NEXT_PUBLIC_ROLLUP_OUTPUT_ROOTS_ENABLED') === 'true',
parentChainName: type === 'arbitrum' ? getEnvValue('NEXT_PUBLIC_ROLLUP_PARENT_CHAIN_NAME') : undefined, parentChainName: type === 'arbitrum' ? getEnvValue('NEXT_PUBLIC_ROLLUP_PARENT_CHAIN_NAME') : undefined,
homepage: { homepage: {
showLatestBlocks: getEnvValue('NEXT_PUBLIC_ROLLUP_HOMEPAGE_SHOW_LATEST_BLOCKS') === 'true', showLatestBlocks: getEnvValue('NEXT_PUBLIC_ROLLUP_HOMEPAGE_SHOW_LATEST_BLOCKS') === 'true',
......
import app from './app'; import app from './app';
import { getEnvValue, getExternalAssetFilePath } from './utils'; import { getEnvValue, getExternalAssetFilePath } from './utils';
const defaultImageUrl = '/static/og_placeholder.png'; const defaultImageUrl = '/static/og_image.png';
const meta = Object.freeze({ const meta = Object.freeze({
promoteBlockscoutInTitle: getEnvValue('NEXT_PUBLIC_PROMOTE_BLOCKSCOUT_IN_TITLE') === 'false' ? false : true, promoteBlockscoutInTitle: getEnvValue('NEXT_PUBLIC_PROMOTE_BLOCKSCOUT_IN_TITLE') === 'false' ? false : true,
......
...@@ -13,23 +13,31 @@ NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL=ws ...@@ -13,23 +13,31 @@ NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL=ws
NEXT_PUBLIC_AD_ADBUTLER_CONFIG_DESKTOP={ "id": "632019", "width": "728", "height": "90" } NEXT_PUBLIC_AD_ADBUTLER_CONFIG_DESKTOP={ "id": "632019", "width": "728", "height": "90" }
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_AD_BANNER_PROVIDER=adbutler NEXT_PUBLIC_AD_BANNER_PROVIDER=adbutler
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=zkevm.blockscout.com NEXT_PUBLIC_API_HOST=zkevm.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
NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/zkevm.json NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/zkevm.json
NEXT_PUBLIC_GAME_BADGE_CLAIM_LINK=https://badges.blockscout.com/mint/sherblockHolmesBadge
NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0x25fcb396fc8652dcd0040f677a1dcc6fecff390ecafc815894379a3f254f1aa9 NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0x25fcb396fc8652dcd0040f677a1dcc6fecff390ecafc815894379a3f254f1aa9
NEXT_PUBLIC_HAS_USER_OPS=true
NEXT_PUBLIC_HIDE_INDEXING_ALERT_INT_TXS=true NEXT_PUBLIC_HIDE_INDEXING_ALERT_INT_TXS=true
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs'] NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs']
NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND=linear-gradient(122deg, rgba(162, 41, 197, 1) 0%, rgba(123, 63, 228, 1) 100%) NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG={'background':['linear-gradient(122deg, rgba(162, 41, 197, 1) 0%, rgba(123, 63, 228, 1) 100%)'],'text_color':['rgba(255, 255, 255, 1)']}
NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR=rgba(255, 255, 255, 1) NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
NEXT_PUBLIC_MARKETPLACE_ENABLED=false NEXT_PUBLIC_LOGOUT_URL=https://blockscout-polygon.us.auth0.com/v2/logout
NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-categories/default.json
NEXT_PUBLIC_MARKETPLACE_ENABLED=true
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_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com
NEXT_PUBLIC_METASUITES_ENABLED=true NEXT_PUBLIC_METASUITES_ENABLED=true
NEXT_PUBLIC_NAVIGATION_HIGHLIGHTED_ROUTES=['/apps']
NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18 NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18
NEXT_PUBLIC_NETWORK_CURRENCY_NAME=MATIC NEXT_PUBLIC_NETWORK_CURRENCY_NAME=ETH
NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=MATIC 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':'/polygon-zkevm/pools'}}] 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':'/polygon-zkevm/pools'}}]
NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/polygon-short.svg NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/polygon-short.svg
NEXT_PUBLIC_NETWORK_ID=1101 NEXT_PUBLIC_NETWORK_ID=1101
......
...@@ -35,6 +35,9 @@ export_envs_from_preset() { ...@@ -35,6 +35,9 @@ export_envs_from_preset() {
# If there is a preset, load the environment variables from the its file # If there is a preset, load the environment variables from the its file
export_envs_from_preset export_envs_from_preset
# Generate OG image
node --no-warnings ./og_image_generator.js
# Download external assets # Download external assets
./download_assets.sh ./public/assets/configs ./download_assets.sh ./public/assets/configs
...@@ -61,6 +64,9 @@ echo ...@@ -61,6 +64,9 @@ echo
# Create envs.js file with run-time environment variables for the client app # Create envs.js file with run-time environment variables for the client app
./make_envs_script.sh ./make_envs_script.sh
# Generate sitemap.xml and robots.txt files
./sitemap_generator.sh
# Print list of enabled features # Print list of enabled features
node ./feature-reporter.js node ./feature-reporter.js
......
/* eslint-disable no-console */
import fs from 'fs';
import path from 'path';
console.log('🎨 Generating OG image...');
const targetFile = path.resolve(process.cwd(), 'public/static/og_image.png');
function copyPlaceholderImage() {
const sourceFile = path.resolve(process.cwd(), 'public/static/og_placeholder.png');
fs.copyFileSync(sourceFile, targetFile);
}
if (process.env.NEXT_PUBLIC_OG_IMAGE_URL) {
console.log('⏩ NEXT_PUBLIC_OG_IMAGE_URL is set. Skipping OG image generation...');
} else if (!process.env.NEXT_PUBLIC_NETWORK_NAME) {
console.log('⏩ NEXT_PUBLIC_NETWORK_NAME is not set. Copying placeholder image...');
copyPlaceholderImage();
} else if (!process.env.NEXT_PUBLIC_NETWORK_LOGO && !process.env.NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG) {
console.log('⏩ Neither NEXT_PUBLIC_NETWORK_LOGO nor NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG is set. Copying placeholder image...');
copyPlaceholderImage();
} else {
try {
const bannerConfig = JSON.parse(process.env.NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG?.replaceAll('\'', '"') || '{}');
const data = {
title: `${ process.env.NEXT_PUBLIC_NETWORK_NAME } explorer`,
logo_url: process.env.NEXT_PUBLIC_NETWORK_LOGO_DARK ?? process.env.NEXT_PUBLIC_NETWORK_LOGO,
background: bannerConfig.background?.[0],
title_color: bannerConfig.text_color?.[0],
invert_logo: !process.env.NEXT_PUBLIC_NETWORK_LOGO_DARK,
};
console.log('⏳ Making request to OG image generator service...');
const response = await fetch('https://bigs.services.blockscout.com/generate/og', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});
if (response.ok) {
console.log('⬇️ Downloading the image...');
const buffer = await response.arrayBuffer();
const imageBuffer = Buffer.from(buffer);
fs.writeFileSync(targetFile, imageBuffer);
} else {
const payload = response.headers.get('Content-type')?.includes('application/json') ? await response.json() : await response.text();
console.error('🛑 Failed to generate OG image. Response:', payload);
console.log('Copying placeholder image...');
copyPlaceholderImage();
}
} catch (error) {
console.error('🛑 Failed to generate OG image. Error:', error?.message);
console.log('Copying placeholder image...');
copyPlaceholderImage();
}
}
console.log('✅ Done.');
cd ./deploy/tools/sitemap-generator
yarn next-sitemap
\ No newline at end of file
...@@ -484,9 +484,9 @@ ms@2.1.2: ...@@ -484,9 +484,9 @@ ms@2.1.2:
integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
nanoid@^3.3.7: nanoid@^3.3.7:
version "3.3.7" version "3.3.8"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.8.tgz#b1be3030bee36aaff18bacb375e5cce521684baf"
integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g== integrity sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==
node-source-walk@^6.0.0, node-source-walk@^6.0.1, node-source-walk@^6.0.2: node-source-walk@^6.0.0, node-source-walk@^6.0.1, node-source-walk@^6.0.2:
version "6.0.2" version "6.0.2"
......
...@@ -587,7 +587,21 @@ const schema = yup ...@@ -587,7 +587,21 @@ const schema = yup
NEXT_PUBLIC_NETWORK_NAME: yup.string().required(), NEXT_PUBLIC_NETWORK_NAME: yup.string().required(),
NEXT_PUBLIC_NETWORK_SHORT_NAME: yup.string(), NEXT_PUBLIC_NETWORK_SHORT_NAME: yup.string(),
NEXT_PUBLIC_NETWORK_ID: yup.number().positive().integer().required(), NEXT_PUBLIC_NETWORK_ID: yup.number().positive().integer().required(),
NEXT_PUBLIC_NETWORK_RPC_URL: yup.string().test(urlTest), NEXT_PUBLIC_NETWORK_RPC_URL: yup
.mixed()
.test(
'shape',
'Invalid schema were provided for NEXT_PUBLIC_NETWORK_RPC_URL, it should be either array of URLs or URL string',
(data) => {
const isUrlSchema = yup.string().test(urlTest);
const isArrayOfUrlsSchema = yup
.array()
.transform(replaceQuotes)
.json()
.of(yup.string().test(urlTest));
return isUrlSchema.isValidSync(data) || isArrayOfUrlsSchema.isValidSync(data);
}),
NEXT_PUBLIC_NETWORK_CURRENCY_NAME: yup.string(), NEXT_PUBLIC_NETWORK_CURRENCY_NAME: yup.string(),
NEXT_PUBLIC_NETWORK_CURRENCY_WEI_NAME: yup.string(), NEXT_PUBLIC_NETWORK_CURRENCY_WEI_NAME: yup.string(),
NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL: yup.string(), NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL: yup.string(),
......
...@@ -5,4 +5,5 @@ NEXT_PUBLIC_HOMEPAGE_STATS=[] ...@@ -5,4 +5,5 @@ NEXT_PUBLIC_HOMEPAGE_STATS=[]
NEXT_PUBLIC_VIEWS_ADDRESS_FORMAT=['base16','bech32'] NEXT_PUBLIC_VIEWS_ADDRESS_FORMAT=['base16','bech32']
NEXT_PUBLIC_VIEWS_ADDRESS_BECH_32_PREFIX=foo NEXT_PUBLIC_VIEWS_ADDRESS_BECH_32_PREFIX=foo
NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY=xxx NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY=xxx
NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY=deprecated NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY=deprecated
\ No newline at end of file NEXT_PUBLIC_NETWORK_RPC_URL=['https://example.com','https://example2.com']
...@@ -340,9 +340,9 @@ commander@^2.20.0: ...@@ -340,9 +340,9 @@ commander@^2.20.0:
integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
cross-spawn@^7.0.3: cross-spawn@^7.0.3:
version "7.0.3" version "7.0.6"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f"
integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==
dependencies: dependencies:
path-key "^3.1.0" path-key "^3.1.0"
shebang-command "^2.0.0" shebang-command "^2.0.0"
......
...@@ -460,9 +460,9 @@ commander@^2.20.0: ...@@ -460,9 +460,9 @@ commander@^2.20.0:
integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
cross-spawn@^7.0.3: cross-spawn@^7.0.3:
version "7.0.3" version "7.0.6"
resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f"
integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==
dependencies: dependencies:
path-key "^3.1.0" path-key "^3.1.0"
shebang-command "^2.0.0" shebang-command "^2.0.0"
......
...@@ -357,9 +357,9 @@ commander@^9.0.0: ...@@ -357,9 +357,9 @@ commander@^9.0.0:
integrity sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ== integrity sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==
cross-spawn@^7.0.3: cross-spawn@^7.0.3:
version "7.0.3" version "7.0.6"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f"
integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==
dependencies: dependencies:
path-key "^3.1.0" path-key "^3.1.0"
shebang-command "^2.0.0" shebang-command "^2.0.0"
......
/node_modules
\ No newline at end of file
/* eslint-disable no-console */
const path = require('path');
const stripTrailingSlash = (str) => str[str.length - 1] === '/' ? str.slice(0, -1) : str;
const fetchResource = async(url, formatter) => {
console.log('🌀 [next-sitemap] Fetching resource:', url);
try {
const res = await fetch(url);
if (res.ok) {
const data = await res.json();
console.log('✅ [next-sitemap] Data fetched for resource:', url);
return formatter(data);
}
} catch (error) {
console.log('🚨 [next-sitemap] Error fetching resource:', url, error);
}
};
const siteUrl = [
process.env.NEXT_PUBLIC_APP_PROTOCOL || 'https',
'://',
process.env.NEXT_PUBLIC_APP_HOST,
process.env.NEXT_PUBLIC_APP_PORT && ':' + process.env.NEXT_PUBLIC_APP_PORT,
].filter(Boolean).join('');
const apiUrl = (() => {
const baseUrl = [
process.env.NEXT_PUBLIC_API_PROTOCOL || 'https',
'://',
process.env.NEXT_PUBLIC_API_HOST,
process.env.NEXT_PUBLIC_API_PORT && ':' + process.env.NEXT_PUBLIC_API_PORT,
].filter(Boolean).join('');
const basePath = stripTrailingSlash(process.env.NEXT_PUBLIC_API_BASE_PATH || '');
return `${ baseUrl }${ basePath }/api/v2`;
})();
/** @type {import('next-sitemap').IConfig} */
module.exports = {
siteUrl,
generateIndexSitemap: false,
generateRobotsTxt: true,
sourceDir: path.resolve(process.cwd(), '../../../.next'),
outDir: path.resolve(process.cwd(), '../../../public'),
exclude: [
'/account/*',
'/auth/*',
'/login',
'/sprite',
],
transform: async(config, path) => {
switch (path) {
case '/mud-worlds':
if (process.env.NEXT_PUBLIC_HAS_MUD_FRAMEWORK !== 'true') {
return null;
}
break;
case '/batches':
case '/deposits':
if (!process.env.NEXT_PUBLIC_ROLLUP_TYPE) {
return null;
}
break;
case '/withdrawals':
if (!process.env.NEXT_PUBLIC_ROLLUP_TYPE && process.env.NEXT_PUBLIC_HAS_BEACON_CHAIN !== 'true') {
return null;
}
break;
case '/dispute-games':
if (process.env.NEXT_PUBLIC_ROLLUP_TYPE !== 'optimistic') {
return null;
}
break;
case '/blobs':
if (process.env.NEXT_PUBLIC_DATA_AVAILABILITY_ENABLED !== 'true') {
return null;
}
break;
case '/name-domains':
if (!process.env.NEXT_PUBLIC_NAME_SERVICE_API_HOST) {
return null;
}
break;
case '/ops':
if (process.env.NEXT_PUBLIC_HAS_USER_OPS !== 'true') {
return null;
}
break;
case '/output-roots':
if (process.env.NEXT_PUBLIC_ROLLUP_OUTPUT_ROOTS_ENABLED !== 'true') {
return null;
}
break;
case '/pools':
if (process.env.NEXT_PUBLIC_DEX_POOLS_ENABLED !== 'true') {
return null;
}
break;
case '/advanced-filter':
if (process.env.NEXT_PUBLIC_ADVANCED_FILTER_ENABLED === 'false') {
return null;
}
break;
case '/apps':
if (process.env.NEXT_PUBLIC_MARKETPLACE_ENABLED !== 'true') {
return null;
}
break;
case '/api-docs':
if (process.env.NEXT_PUBLIC_API_SPEC_URL === 'none') {
return null;
}
break;
case '/gas-tracker':
if (process.env.NEXT_PUBLIC_GAS_TRACKER_ENABLED === 'false') {
return null;
}
break;
case '/graphql':
if (process.env.NEXT_PUBLIC_GRAPHIQL_TRANSACTION === 'none') {
return null;
}
break;
case '/stats':
if (!process.env.NEXT_PUBLIC_STATS_API_HOST) {
return null;
}
break;
case '/validators':
if (!process.env.NEXT_PUBLIC_VALIDATORS_CHAIN_TYPE) {
return null;
}
break;
}
return {
loc: path,
changefreq: undefined,
priority: undefined,
lastmod: config.autoLastmod ? new Date().toISOString() : undefined,
alternateRefs: config.alternateRefs ?? [],
};
},
additionalPaths: async(config) => {
const addresses = fetchResource(
`${ apiUrl }/addresses`,
(data) => data.items.map(({ hash }) => `/address/${ hash }`),
);
const txs = fetchResource(
`${ apiUrl }/transactions?filter=validated`,
(data) => data.items.map(({ hash }) => `/tx/${ hash }`),
);
const blocks = fetchResource(
`${ apiUrl }/blocks?type=block`,
(data) => data.items.map(({ height }) => `/block/${ height }`),
);
const tokens = fetchResource(
`${ apiUrl }/tokens`,
(data) => data.items.map(({ address }) => `/token/${ address }`),
);
const contracts = fetchResource(
`${ apiUrl }/smart-contracts`,
(data) => data.items.map(({ address }) => `/address/${ address.hash }?tab=contract`),
);
return Promise.all([
...(await addresses || []),
...(await txs || []),
...(await blocks || []),
...(await tokens || []),
...(await contracts || []),
].map(path => config.transform(config, path)));
},
};
{
"name": "sitemap-generator",
"version": "1.0.0",
"main": "index.js",
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"next-sitemap": "4.2.3"
}
}
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
"@corex/deepmerge@^4.0.43":
version "4.0.43"
resolved "https://registry.yarnpkg.com/@corex/deepmerge/-/deepmerge-4.0.43.tgz#9bd42559ebb41cc5a7fb7cfeea5f231c20977dca"
integrity sha512-N8uEMrMPL0cu/bdboEWpQYb/0i2K5Qn8eCsxzOmxSggJbbQte7ljMRoXm917AbntqTGOzdTu+vP3KOOzoC70HQ==
"@next/env@^13.4.3":
version "13.5.8"
resolved "https://registry.yarnpkg.com/@next/env/-/env-13.5.8.tgz#404d3b3e5881b6a0510500c6cc97e3589a2e6371"
integrity sha512-YmiG58BqyZ2FjrF2+5uZExL2BrLr8RTQzLXNDJ8pJr0O+rPlOeDPXp1p1/4OrR3avDidzZo3D8QO2cuDv1KCkw==
"@nodelib/fs.scandir@2.1.5":
version "2.1.5"
resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==
dependencies:
"@nodelib/fs.stat" "2.0.5"
run-parallel "^1.1.9"
"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2":
version "2.0.5"
resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b"
integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==
"@nodelib/fs.walk@^1.2.3":
version "1.2.8"
resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a"
integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==
dependencies:
"@nodelib/fs.scandir" "2.1.5"
fastq "^1.6.0"
braces@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789"
integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==
dependencies:
fill-range "^7.1.1"
fast-glob@^3.2.12:
version "3.3.2"
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129"
integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==
dependencies:
"@nodelib/fs.stat" "^2.0.2"
"@nodelib/fs.walk" "^1.2.3"
glob-parent "^5.1.2"
merge2 "^1.3.0"
micromatch "^4.0.4"
fastq@^1.6.0:
version "1.18.0"
resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.18.0.tgz#d631d7e25faffea81887fe5ea8c9010e1b36fee0"
integrity sha512-QKHXPW0hD8g4UET03SdOdunzSouc9N4AuHdsX8XNcTsuz+yYFILVNIX4l9yHABMhiEI9Db0JTTIpu0wB+Y1QQw==
dependencies:
reusify "^1.0.4"
fill-range@^7.1.1:
version "7.1.1"
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292"
integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==
dependencies:
to-regex-range "^5.0.1"
glob-parent@^5.1.2:
version "5.1.2"
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
dependencies:
is-glob "^4.0.1"
is-extglob@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==
is-glob@^4.0.1:
version "4.0.3"
resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084"
integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==
dependencies:
is-extglob "^2.1.1"
is-number@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
merge2@^1.3.0:
version "1.4.1"
resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
micromatch@^4.0.4:
version "4.0.8"
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202"
integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==
dependencies:
braces "^3.0.3"
picomatch "^2.3.1"
minimist@^1.2.8:
version "1.2.8"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c"
integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==
next-sitemap@4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/next-sitemap/-/next-sitemap-4.2.3.tgz#5db3f650351a51e84b9fd6b58c5af2f9257b5058"
integrity sha512-vjdCxeDuWDzldhCnyFCQipw5bfpl4HmZA7uoo3GAaYGjGgfL4Cxb1CiztPuWGmS+auYs7/8OekRS8C2cjdAsjQ==
dependencies:
"@corex/deepmerge" "^4.0.43"
"@next/env" "^13.4.3"
fast-glob "^3.2.12"
minimist "^1.2.8"
picomatch@^2.3.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
queue-microtask@^1.2.2:
version "1.2.3"
resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
reusify@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
run-parallel@^1.1.9:
version "1.2.0"
resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee"
integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==
dependencies:
queue-microtask "^1.2.2"
to-regex-range@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==
dependencies:
is-number "^7.0.0"
...@@ -95,7 +95,7 @@ Please be aware that all environment variables prefixed with `NEXT_PUBLIC_` will ...@@ -95,7 +95,7 @@ Please be aware that all environment variables prefixed with `NEXT_PUBLIC_` will
| NEXT_PUBLIC_NETWORK_NAME | `string` | Displayed name of the network | Required | - | `Gnosis Chain` | v1.0.x+ | | NEXT_PUBLIC_NETWORK_NAME | `string` | Displayed name of the network | Required | - | `Gnosis Chain` | v1.0.x+ |
| NEXT_PUBLIC_NETWORK_SHORT_NAME | `string` | Used for SEO attributes (e.g, page description) | - | - | `OoG` | v1.0.x+ | | NEXT_PUBLIC_NETWORK_SHORT_NAME | `string` | Used for SEO attributes (e.g, page description) | - | - | `OoG` | v1.0.x+ |
| NEXT_PUBLIC_NETWORK_ID | `number` | Chain id, see [https://chainlist.org](https://chainlist.org) for the reference | Required | - | `99` | v1.0.x+ | | NEXT_PUBLIC_NETWORK_ID | `number` | Chain id, see [https://chainlist.org](https://chainlist.org) for the reference | Required | - | `99` | v1.0.x+ |
| NEXT_PUBLIC_NETWORK_RPC_URL | `string` | Chain public RPC server url, see [https://chainlist.org](https://chainlist.org) for the reference | - | - | `https://core.poa.network` | v1.0.x+ | | NEXT_PUBLIC_NETWORK_RPC_URL | `string \| Array<string>` | Chain public RPC server url, see [https://chainlist.org](https://chainlist.org) for the reference. Can contain a single string value, or an array of urls. | - | - | `https://core.poa.network` | v1.0.x+ |
| NEXT_PUBLIC_NETWORK_CURRENCY_NAME | `string` | Network currency name | - | - | `Ether` | v1.0.x+ | | NEXT_PUBLIC_NETWORK_CURRENCY_NAME | `string` | Network currency name | - | - | `Ether` | v1.0.x+ |
| NEXT_PUBLIC_NETWORK_CURRENCY_WEI_NAME | `string` | Name of network currency subdenomination | - | `wei` | `duck` | v1.23.0+ | | NEXT_PUBLIC_NETWORK_CURRENCY_WEI_NAME | `string` | Name of network currency subdenomination | - | `wei` | `duck` | v1.23.0+ |
| NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL | `string` | Network currency symbol | - | - | `ETH` | v1.0.x+ | | NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL | `string` | Network currency symbol | - | - | `ETH` | v1.0.x+ |
...@@ -455,7 +455,7 @@ This feature is **enabled by default** with the `coinzilla` ads provider. To swi ...@@ -455,7 +455,7 @@ This feature is **enabled by default** with the `coinzilla` ads provider. To swi
| NEXT_PUBLIC_FAULT_PROOF_ENABLED | `boolean` | Set to `true` for chains with fault proof system enabled (Optimistic stack only) | - | - | `true` | v1.31.0+ | | NEXT_PUBLIC_FAULT_PROOF_ENABLED | `boolean` | Set to `true` for chains with fault proof system enabled (Optimistic stack only) | - | - | `true` | v1.31.0+ |
| NEXT_PUBLIC_HAS_MUD_FRAMEWORK | `boolean` | Set to `true` for instances that use MUD framework (Optimistic stack only) | - | - | `true` | v1.33.0+ | | NEXT_PUBLIC_HAS_MUD_FRAMEWORK | `boolean` | Set to `true` for instances that use MUD framework (Optimistic stack only) | - | - | `true` | v1.33.0+ |
| NEXT_PUBLIC_ROLLUP_HOMEPAGE_SHOW_LATEST_BLOCKS | `boolean` | Set to `true` to display "Latest blocks" widget instead of "Latest batches" on the home page | - | - | `true` | v1.36.0+ | | NEXT_PUBLIC_ROLLUP_HOMEPAGE_SHOW_LATEST_BLOCKS | `boolean` | Set to `true` to display "Latest blocks" widget instead of "Latest batches" on the home page | - | - | `true` | v1.36.0+ |
| NEXT_PUBLIC_ROLLUP_OUTPUT_ROOTS_ENABLED | `boolean` | Enables "Output roots" page (Optimistic stack only) | - | `true` | `false` | v1.37.0+ | | NEXT_PUBLIC_ROLLUP_OUTPUT_ROOTS_ENABLED | `boolean` | Enables "Output roots" page (Optimistic stack only) | - | `false` | `true` | v1.37.0+ |
| NEXT_PUBLIC_ROLLUP_PARENT_CHAIN_NAME | `string` | Set to customize L1 transaction status labels in the UI (e.g., "Sent to <chain-name>"). This setting is applicable only for Arbitrum-based chains. | - | - | `DuckChain` | v1.37.0+ | | NEXT_PUBLIC_ROLLUP_PARENT_CHAIN_NAME | `string` | Set to customize L1 transaction status labels in the UI (e.g., "Sent to <chain-name>"). This setting is applicable only for Arbitrum-based chains. | - | - | `DuckChain` | v1.37.0+ |
&nbsp; &nbsp;
......
...@@ -30,14 +30,9 @@ const RESTRICTED_MODULES = { ...@@ -30,14 +30,9 @@ const RESTRICTED_MODULES = {
importNames: [ 'Popover', 'Menu', 'PinInput', 'useToast', 'Skeleton' ], importNames: [ 'Popover', 'Menu', 'PinInput', 'useToast', 'Skeleton' ],
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',
}, },
{
name: 'lodash',
message: 'Please use `import [package] from \'lodash/[package]\'` instead.',
},
], ],
patterns: [ patterns: [
'icons/*', 'icons/*',
'!lodash/*',
], ],
}; };
...@@ -428,6 +423,7 @@ export default tseslint.config( ...@@ -428,6 +423,7 @@ export default tseslint.config(
'pages/**', 'pages/**',
'nextjs/**', 'nextjs/**',
'playwright/**', 'playwright/**',
'deploy/scripts/**',
'deploy/tools/**', 'deploy/tools/**',
'middleware.ts', 'middleware.ts',
'instrumentation*.ts', 'instrumentation*.ts',
......
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.444 18.315a10 10 0 1 0 11.112-16.63 10 10 0 0 0-11.112 16.63ZM5.3 2.965a8.462 8.462 0 1 1 9.402 14.07 8.462 8.462 0 0 1-9.402-14.07Zm8.637 11.978a.768.768 0 0 0 .295.057.769.769 0 0 0 .546-1.315l-4.008-4V3.846a.769.769 0 1 0-1.538 0V10a.77.77 0 0 0 .223.546l4.23 4.23a.77.77 0 0 0 .252.167Z" fill="currentColor"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M4.444 18.315a10 10 0 1 0 11.112-16.63 10 10 0 0 0-11.112 16.63ZM5.3 2.965a8.462 8.462 0 1 1 9.402 14.07A8.462 8.462 0 0 1 5.3 2.965Zm8.637 11.978a.768.768 0 0 0 .295.057.769.769 0 0 0 .546-1.315l-4.008-4V3.846a.769.769 0 1 0-1.538 0V10a.77.77 0 0 0 .223.546l4.23 4.23a.77.77 0 0 0 .252.167Z" fill="currentColor"/>
</svg> </svg>
import { getAddress } from 'viem'; import { getAddress } from 'viem';
import config from 'configs/app';
export default function getCheckedSummedAddress(address: string): string { export default function getCheckedSummedAddress(address: string): string {
try { try {
return getAddress(address); return getAddress(
address,
// We need to pass chainId to getAddress to make it work correctly for some chains, e.g. Rootstock
config.chain.id ? Number(config.chain.id) : undefined,
);
} catch (error) { } catch (error) {
return address; return address;
} }
......
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import _omit from 'lodash/omit'; import { omit, pickBy } from 'es-toolkit';
import _pickBy from 'lodash/pickBy';
import React from 'react'; import React from 'react';
import type { CsrfData } from 'types/client/account'; import type { CsrfData } from 'types/client/account';
...@@ -38,7 +37,7 @@ export default function useApiFetch() { ...@@ -38,7 +37,7 @@ export default function useApiFetch() {
const resource: ApiResource = RESOURCES[resourceName]; const resource: ApiResource = RESOURCES[resourceName];
const url = buildUrl(resourceName, pathParams, queryParams); const url = buildUrl(resourceName, pathParams, queryParams);
const withBody = isBodyAllowed(fetchParams?.method); const withBody = isBodyAllowed(fetchParams?.method);
const headers = _pickBy({ const headers = pickBy({
'x-endpoint': resource.endpoint && isNeedProxy() ? resource.endpoint : undefined, 'x-endpoint': resource.endpoint && isNeedProxy() ? resource.endpoint : undefined,
Authorization: resource.endpoint && resource.needAuth ? apiToken : undefined, Authorization: resource.endpoint && resource.needAuth ? apiToken : undefined,
'x-csrf-token': withBody && csrfToken ? csrfToken : undefined, 'x-csrf-token': withBody && csrfToken ? csrfToken : undefined,
...@@ -55,7 +54,7 @@ export default function useApiFetch() { ...@@ -55,7 +54,7 @@ export default function useApiFetch() {
// change condition here if something is changed // change condition here if something is changed
credentials: config.features.account.isEnabled ? 'include' : 'same-origin', credentials: config.features.account.isEnabled ? 'include' : 'same-origin',
headers, headers,
..._omit(fetchParams, 'headers'), ...(fetchParams ? omit(fetchParams, [ 'headers' ]) : {}),
}, },
{ {
resource: resource.path, resource: resource.path,
......
import clamp from 'lodash/clamp'; import { throttle, clamp } from 'es-toolkit';
import throttle from 'lodash/throttle';
import React from 'react'; import React from 'react';
const ScrollDirectionContext = React.createContext<'up' | 'down' | null>(null); const ScrollDirectionContext = React.createContext<'up' | 'down' | null>(null);
......
import _debounce from 'lodash/debounce'; import { debounce } from 'es-toolkit';
import type { LegacyRef } from 'react'; import type { LegacyRef } from 'react';
import React from 'react'; import React from 'react';
...@@ -19,7 +19,7 @@ export default function useClientRect<E extends Element>(): [ DOMRect | null, Le ...@@ -19,7 +19,7 @@ export default function useClientRect<E extends Element>(): [ DOMRect | null, Le
return; return;
} }
const resizeHandler = _debounce(() => { const resizeHandler = debounce(() => {
setRect(nodeRef.current?.getBoundingClientRect() ?? null); setRect(nodeRef.current?.getBoundingClientRect() ?? null);
}, 100); }, 100);
......
import throttle from 'lodash/throttle'; import { throttle } from 'es-toolkit';
import React from 'react'; import React from 'react';
export default function useIsSticky(ref: React.RefObject<HTMLDivElement>, offset = 0, isEnabled = true) { export default function useIsSticky(ref: React.RefObject<HTMLDivElement>, offset = 0, isEnabled = true) {
......
import _clamp from 'lodash/clamp'; import { clamp } from 'es-toolkit';
import React from 'react'; import React from 'react';
import { useInView } from 'react-intersection-observer'; import { useInView } from 'react-intersection-observer';
...@@ -15,7 +15,7 @@ export default function useLazyRenderedList(list: Array<unknown>, isEnabled: boo ...@@ -15,7 +15,7 @@ export default function useLazyRenderedList(list: Array<unknown>, isEnabled: boo
React.useEffect(() => { React.useEffect(() => {
if (inView) { if (inView) {
setRenderedItemsNum((prev) => _clamp(prev + STEP, 0, list.length)); setRenderedItemsNum((prev) => clamp(prev + STEP, 0, list.length));
} }
}, [ inView, list.length ]); }, [ inView, list.length ]);
......
...@@ -32,7 +32,7 @@ exports[`generates correct metadata for: static route 1`] = ` ...@@ -32,7 +32,7 @@ exports[`generates correct metadata for: static route 1`] = `
"description": "Open-source block explorer by Blockscout. Search transactions, verify smart contracts, analyze addresses, and track network activity. Complete blockchain data and APIs for the Blockscout (Blockscout) Explorer network.", "description": "Open-source block explorer by Blockscout. Search transactions, verify smart contracts, analyze addresses, and track network activity. Complete blockchain data and APIs for the Blockscout (Blockscout) Explorer network.",
"opengraph": { "opengraph": {
"description": "", "description": "",
"imageUrl": "http://localhost:3000/static/og_placeholder.png", "imageUrl": "http://localhost:3000/static/og_image.png",
"title": "Blockscout transactions - Blockscout explorer | Blockscout", "title": "Blockscout transactions - Blockscout explorer | Blockscout",
}, },
"title": "Blockscout transactions - Blockscout explorer | Blockscout", "title": "Blockscout transactions - Blockscout explorer | Blockscout",
......
import _capitalize from 'lodash/capitalize'; import { capitalize } from 'es-toolkit';
export default function getTabName(tab: string) { export default function getTabName(tab: string) {
return tab !== '' ? _capitalize(tab.replaceAll('_', ' ')) : 'Default'; return tab !== '' ? capitalize(tab.replaceAll('_', ' ')) : 'Default';
} }
import _capitalize from 'lodash/capitalize'; import { capitalize } from 'es-toolkit';
import type { Config } from 'mixpanel-browser'; import type { Config } from 'mixpanel-browser';
import mixpanel from 'mixpanel-browser'; import mixpanel from 'mixpanel-browser';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
...@@ -40,12 +40,12 @@ export default function useMixpanelInit() { ...@@ -40,12 +40,12 @@ export default function useMixpanelInit() {
'Viewport width': window.innerWidth, 'Viewport width': window.innerWidth,
'Viewport height': window.innerHeight, 'Viewport height': window.innerHeight,
Language: window.navigator.language, Language: window.navigator.language,
'Device type': _capitalize(deviceType), 'Device type': capitalize(deviceType),
'User id': userId, 'User id': userId,
}); });
mixpanel.identify(userId); mixpanel.identify(userId);
userProfile.set({ userProfile.set({
'Device Type': _capitalize(deviceType), 'Device Type': capitalize(deviceType),
...(isAuth ? { 'With Account': true } : {}), ...(isAuth ? { 'With Account': true } : {}),
}); });
userProfile.setOnce({ userProfile.setOnce({
......
import _compose from 'lodash/fp/compose'; import { mapValues } from 'es-toolkit';
import _mapValues from 'lodash/mapValues';
import type { NetworkExplorer } from 'types/networks'; import type { NetworkExplorer } from 'types/networks';
...@@ -32,7 +31,7 @@ const networkExplorers: Array<NetworkExplorer> = (() => { ...@@ -32,7 +31,7 @@ const networkExplorers: Array<NetworkExplorer> = (() => {
return config.UI.explorers.items.map((explorer) => ({ return config.UI.explorers.items.map((explorer) => ({
...explorer, ...explorer,
baseUrl: stripTrailingSlash(explorer.baseUrl), baseUrl: stripTrailingSlash(explorer.baseUrl),
paths: _mapValues(explorer.paths, _compose(stripTrailingSlash, addLeadingSlash)), paths: mapValues(explorer.paths, (value) => value ? stripTrailingSlash(addLeadingSlash(value)) : value),
})); }));
})(); })();
......
import _uniq from 'lodash/uniq'; import { uniq } from 'es-toolkit';
import isBrowser from './isBrowser'; import isBrowser from './isBrowser';
...@@ -27,7 +27,7 @@ export function saveToRecentKeywords(value: string) { ...@@ -27,7 +27,7 @@ export function saveToRecentKeywords(value: string) {
} }
const keywordsArr = getRecentSearchKeywords(); const keywordsArr = getRecentSearchKeywords();
const result = _uniq([ value, ...keywordsArr ]).slice(0, MAX_KEYWORDS_NUMBER - 1); const result = uniq([ value, ...keywordsArr ]).slice(0, MAX_KEYWORDS_NUMBER - 1);
window.localStorage.setItem(RECENT_KEYWORDS_LS_KEY, JSON.stringify(result)); window.localStorage.setItem(RECENT_KEYWORDS_LS_KEY, JSON.stringify(result));
} }
......
import _upperFirst from 'lodash/upperFirst'; import { upperFirst } from 'es-toolkit';
import type { Metadata, MetadataAttributes } from 'types/client/token'; import type { Metadata, MetadataAttributes } from 'types/client/token';
...@@ -72,7 +72,7 @@ export default function attributesParser(attributes: Array<unknown>): Metadata[' ...@@ -72,7 +72,7 @@ export default function attributesParser(attributes: Array<unknown>): Metadata['
return { return {
...formatValue(value, display, trait), ...formatValue(value, display, trait),
trait_type: _upperFirst(trait || 'property'), trait_type: upperFirst(trait || 'property'),
}; };
}) })
.filter((item) => item?.value) .filter((item) => item?.value)
......
...@@ -12,7 +12,7 @@ const currentChain = { ...@@ -12,7 +12,7 @@ const currentChain = {
}, },
rpcUrls: { rpcUrls: {
'default': { 'default': {
http: [ config.chain.rpcUrl ?? '' ], http: config.chain.rpcUrls,
}, },
}, },
blockExplorers: { blockExplorers: {
......
import _get from 'lodash/get'; import { get } from 'es-toolkit/compat';
import React from 'react'; import React from 'react';
import config from 'configs/app'; import config from 'configs/app';
...@@ -25,7 +25,7 @@ export default function useAddOrSwitchChain() { ...@@ -25,7 +25,7 @@ export default function useAddOrSwitchChain() {
const errorObj = getErrorObj(error); const errorObj = getErrorObj(error);
const code = errorObj && 'code' in errorObj ? errorObj.code : undefined; const code = errorObj && 'code' in errorObj ? errorObj.code : undefined;
const originalErrorCode = _get(errorObj, 'data.originalError.code'); const originalErrorCode = get(errorObj, 'data.originalError.code');
// This error code indicates that the chain has not been added to Wallet. // This error code indicates that the chain has not been added to Wallet.
if (code === 4902 || originalErrorCode === 4902) { if (code === 4902 || originalErrorCode === 4902) {
...@@ -37,7 +37,7 @@ export default function useAddOrSwitchChain() { ...@@ -37,7 +37,7 @@ export default function useAddOrSwitchChain() {
symbol: config.chain.currency.symbol, symbol: config.chain.currency.symbol,
decimals: config.chain.currency.decimals, decimals: config.chain.currency.decimals,
}, },
rpcUrls: [ config.chain.rpcUrl ], rpcUrls: config.chain.rpcUrls,
blockExplorerUrls: [ config.app.baseUrl ], blockExplorerUrls: [ config.app.baseUrl ],
} ] as never; } ] as never;
// in wagmi types for wallet_addEthereumChain method is not provided // in wagmi types for wallet_addEthereumChain method is not provided
......
import { WagmiAdapter } from '@reown/appkit-adapter-wagmi'; import { WagmiAdapter } from '@reown/appkit-adapter-wagmi';
import { http } from 'viem'; import { fallback, http } from 'viem';
import { createConfig } from 'wagmi'; import { createConfig } from 'wagmi';
import config from 'configs/app'; import config from 'configs/app';
...@@ -13,7 +13,11 @@ const wagmi = (() => { ...@@ -13,7 +13,11 @@ const wagmi = (() => {
const wagmiConfig = createConfig({ const wagmiConfig = createConfig({
chains: [ currentChain ], chains: [ currentChain ],
transports: { transports: {
[currentChain.id]: http(config.chain.rpcUrl || `${ config.api.endpoint }/api/eth-rpc`), [currentChain.id]: fallback(
config.chain.rpcUrls
.map((url) => http(url))
.concat(http(`${ config.api.endpoint }/api/eth-rpc`)),
),
}, },
ssr: true, ssr: true,
batch: { multicall: { wait: 100 } }, batch: { multicall: { wait: 100 } },
...@@ -26,7 +30,7 @@ const wagmi = (() => { ...@@ -26,7 +30,7 @@ const wagmi = (() => {
networks: chains, networks: chains,
multiInjectedProviderDiscovery: true, multiInjectedProviderDiscovery: true,
transports: { transports: {
[currentChain.id]: http(), [currentChain.id]: fallback(config.chain.rpcUrls.map((url) => http(url))),
}, },
projectId: feature.walletConnect.projectId, projectId: feature.walletConnect.projectId,
ssr: true, ssr: true,
......
import _padStart from 'lodash/padStart'; import { padStart } from 'es-toolkit/compat';
import type { BlockEpoch, BlockEpochElectionRewardDetails, BlockEpochElectionRewardDetailsResponse } from 'types/api/block'; import type { BlockEpoch, BlockEpochElectionRewardDetails, BlockEpochElectionRewardDetailsResponse } from 'types/api/block';
...@@ -42,11 +42,11 @@ function getRewardDetailsItem(index: number): BlockEpochElectionRewardDetails { ...@@ -42,11 +42,11 @@ function getRewardDetailsItem(index: number): BlockEpochElectionRewardDetails {
amount: `${ 100 - index }210001063118670575`, amount: `${ 100 - index }210001063118670575`,
account: { account: {
...addressMock.withoutName, ...addressMock.withoutName,
hash: `0x30D060F129817c4DE5fBc1366d53e19f43c8c6${ _padStart(String(index), 2, '0') }`, hash: `0x30D060F129817c4DE5fBc1366d53e19f43c8c6${ padStart(String(index), 2, '0') }`,
}, },
associated_account: { associated_account: {
...addressMock.withoutName, ...addressMock.withoutName,
hash: `0x456f41406B32c45D59E539e4BBA3D7898c3584${ _padStart(String(index), 2, '0') }`, hash: `0x456f41406B32c45D59E539e4BBA3D7898c3584${ padStart(String(index), 2, '0') }`,
}, },
}; };
} }
......
import _mapValues from 'lodash/mapValues'; import { mapValues } from 'es-toolkit';
import type { HomeStats } from 'types/api/stats'; import type { HomeStats } from 'types/api/stats';
...@@ -51,17 +51,17 @@ export const withBtcLocked: HomeStats = { ...@@ -51,17 +51,17 @@ export const withBtcLocked: HomeStats = {
export const withoutFiatPrices: HomeStats = { export const withoutFiatPrices: HomeStats = {
...base, ...base,
gas_prices: _mapValues(base.gas_prices, (price) => price ? ({ ...price, fiat_price: null }) : null), gas_prices: base.gas_prices ? mapValues(base.gas_prices, (price) => price ? ({ ...price, fiat_price: null }) : null) : null,
}; };
export const withoutGweiPrices: HomeStats = { export const withoutGweiPrices: HomeStats = {
...base, ...base,
gas_prices: _mapValues(base.gas_prices, (price) => price ? ({ ...price, price: null }) : null), gas_prices: base.gas_prices ? mapValues(base.gas_prices, (price) => price ? ({ ...price, price: null }) : null) : null,
}; };
export const withoutBothPrices: HomeStats = { export const withoutBothPrices: HomeStats = {
...base, ...base,
gas_prices: _mapValues(base.gas_prices, (price) => price ? ({ ...price, price: null, fiat_price: null }) : null), gas_prices: base.gas_prices ? mapValues(base.gas_prices, (price) => price ? ({ ...price, price: null, fiat_price: null }) : null) : null,
}; };
export const withoutGasInfo: HomeStats = { export const withoutGasInfo: HomeStats = {
......
...@@ -51,7 +51,7 @@ export function app(): CspDev.DirectiveDescriptor { ...@@ -51,7 +51,7 @@ export function app(): CspDev.DirectiveDescriptor {
getFeaturePayload(config.features.rewards)?.api.endpoint, getFeaturePayload(config.features.rewards)?.api.endpoint,
// chain RPC server // chain RPC server
config.chain.rpcUrl, ...config.chain.rpcUrls,
'https://infragrid.v.network', // RPC providers 'https://infragrid.v.network', // RPC providers
// github (spec for api-docs page) // github (spec for api-docs page)
......
import type CspDev from 'csp-dev'; import type CspDev from 'csp-dev';
import { uniq } from 'es-toolkit';
export const KEY_WORDS = { export const KEY_WORDS = {
BLOB: 'blob:', BLOB: 'blob:',
...@@ -11,17 +12,6 @@ export const KEY_WORDS = { ...@@ -11,17 +12,6 @@ export const KEY_WORDS = {
UNSAFE_EVAL: '\'unsafe-eval\'', UNSAFE_EVAL: '\'unsafe-eval\'',
}; };
// we cannot use lodash/uniq and lodash/mergeWith in middleware code since it calls new Set() and it'is causing an error in Next.js
// "Dynamic Code Evaluation (e. g. 'eval', 'new Function', 'WebAssembly.compile') not allowed in Edge Runtime"
export function unique(array: Array<string | undefined>) {
const set: Record<string, boolean> = {};
for (const item of array) {
item && (set[item] = true);
}
return Object.keys(set);
}
export function mergeDescriptors(...descriptors: Array<CspDev.DirectiveDescriptor>) { export function mergeDescriptors(...descriptors: Array<CspDev.DirectiveDescriptor>) {
return descriptors.reduce((result, item) => { return descriptors.reduce((result, item) => {
for (const _key in item) { for (const _key in item) {
...@@ -50,7 +40,7 @@ export function makePolicyString(policyDescriptor: CspDev.DirectiveDescriptor) { ...@@ -50,7 +40,7 @@ export function makePolicyString(policyDescriptor: CspDev.DirectiveDescriptor) {
return; return;
} }
const uniqueValues = unique(value); const uniqueValues = uniq(value);
return [ key, uniqueValues.join(' ') ].join(' '); return [ key, uniqueValues.join(' ') ].join(' ');
}) })
.filter(Boolean) .filter(Boolean)
......
import { pick } from 'es-toolkit';
import type { IncomingMessage } from 'http'; import type { IncomingMessage } from 'http';
import _pick from 'lodash/pick';
import type { NextApiRequest } from 'next'; import type { NextApiRequest } from 'next';
import type { NextApiRequestCookies } from 'next/dist/server/api-utils'; import type { NextApiRequestCookies } from 'next/dist/server/api-utils';
import type { RequestInit, Response } from 'node-fetch'; import type { RequestInit, Response } from 'node-fetch';
...@@ -21,7 +21,7 @@ export default function fetchFactory( ...@@ -21,7 +21,7 @@ export default function fetchFactory(
accept: _req.headers['accept'] || 'application/json', accept: _req.headers['accept'] || 'application/json',
'content-type': _req.headers['content-type'] || 'application/json', 'content-type': _req.headers['content-type'] || 'application/json',
cookie: apiToken ? `${ cookies.NAMES.API_TOKEN }=${ apiToken }` : '', cookie: apiToken ? `${ cookies.NAMES.API_TOKEN }=${ apiToken }` : '',
..._pick(_req.headers, [ ...pick(_req.headers, [
'x-csrf-token', 'x-csrf-token',
'Authorization', // the old value, just in case 'Authorization', // the old value, just in case
'authorization', // Node.js automatically lowercases headers 'authorization', // Node.js automatically lowercases headers
......
...@@ -33,6 +33,8 @@ ...@@ -33,6 +33,8 @@
"test:jest": "jest", "test:jest": "jest",
"test:jest:watch": "jest --watch", "test:jest:watch": "jest --watch",
"favicon:generate:dev": "./tools/scripts/favicon-generator.dev.sh", "favicon:generate:dev": "./tools/scripts/favicon-generator.dev.sh",
"og-image:generate:dev": "./tools/scripts/og-image-generator.dev.sh",
"sitemap:generate:dev": "./tools/scripts/sitemap-generator.dev.sh",
"monitoring:prometheus:local": "docker run --name blockscout_prometheus -d -p 127.0.0.1:9090:9090 -v $(pwd)/prometheus.yml:/etc/prometheus/prometheus.yml prom/prometheus", "monitoring:prometheus:local": "docker run --name blockscout_prometheus -d -p 127.0.0.1:9090:9090 -v $(pwd)/prometheus.yml:/etc/prometheus/prometheus.yml prom/prometheus",
"monitoring:grafana:local": "docker run -d -p 4000:3000 --name=blockscout_grafana --user $(id -u) --volume $(pwd)/grafana:/var/lib/grafana grafana/grafana-enterprise" "monitoring:grafana:local": "docker run -d -p 4000:3000 --name=blockscout_grafana --user $(id -u) --volume $(pwd)/grafana:/var/lib/grafana grafana/grafana-enterprise"
}, },
...@@ -60,10 +62,10 @@ ...@@ -60,10 +62,10 @@
"@opentelemetry/sdk-node": "0.49.1", "@opentelemetry/sdk-node": "0.49.1",
"@opentelemetry/sdk-trace-node": "1.22.0", "@opentelemetry/sdk-trace-node": "1.22.0",
"@opentelemetry/semantic-conventions": "1.22.0", "@opentelemetry/semantic-conventions": "1.22.0",
"@rollbar/react": "0.12.0-beta",
"@scure/base": "1.1.9",
"@reown/appkit": "1.6.0", "@reown/appkit": "1.6.0",
"@reown/appkit-adapter-wagmi": "1.6.0", "@reown/appkit-adapter-wagmi": "1.6.0",
"@rollbar/react": "0.12.0-beta",
"@scure/base": "1.1.9",
"@slise/embed-react": "^2.2.0", "@slise/embed-react": "^2.2.0",
"@tanstack/react-query": "5.55.4", "@tanstack/react-query": "5.55.4",
"@tanstack/react-query-devtools": "5.55.4", "@tanstack/react-query-devtools": "5.55.4",
...@@ -78,6 +80,7 @@ ...@@ -78,6 +80,7 @@
"dappscout-iframe": "0.2.5", "dappscout-iframe": "0.2.5",
"dayjs": "^1.11.5", "dayjs": "^1.11.5",
"dom-to-image": "^2.6.0", "dom-to-image": "^2.6.0",
"es-toolkit": "1.31.0",
"focus-visible": "^5.2.0", "focus-visible": "^5.2.0",
"framer-motion": "^6.5.1", "framer-motion": "^6.5.1",
"getit-sdk": "^1.0.4", "getit-sdk": "^1.0.4",
...@@ -86,7 +89,6 @@ ...@@ -86,7 +89,6 @@
"graphql": "^16.8.1", "graphql": "^16.8.1",
"graphql-ws": "^5.11.3", "graphql-ws": "^5.11.3",
"js-cookie": "^3.0.1", "js-cookie": "^3.0.1",
"lodash": "^4.0.0",
"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",
......
import _pick from 'lodash/pick'; import { pick, pickBy } from 'es-toolkit';
import _pickBy from 'lodash/pickBy';
import type { NextApiRequest, NextApiResponse } from 'next'; import type { NextApiRequest, NextApiResponse } from 'next';
import fetchFactory from 'nextjs/utils/fetchProxy'; import fetchFactory from 'nextjs/utils/fetchProxy';
...@@ -18,7 +17,7 @@ const handler = async(nextReq: NextApiRequest, nextRes: NextApiResponse) => { ...@@ -18,7 +17,7 @@ const handler = async(nextReq: NextApiRequest, nextRes: NextApiResponse) => {
); );
const apiRes = await fetchFactory(nextReq)( const apiRes = await fetchFactory(nextReq)(
url.toString(), url.toString(),
_pickBy(_pick(nextReq, [ 'body', 'method' ]), Boolean), pickBy(pick(nextReq, [ 'body', 'method' ]), Boolean),
); );
// proxy some headers from API // proxy some headers from API
......
import type { TestFixture, Page } from '@playwright/test'; import type { TestFixture, Page } from '@playwright/test';
import _isEqual from 'lodash/isEqual'; import { isEqual } from 'es-toolkit';
import { encodeFunctionData, encodeFunctionResult, type AbiFunction } from 'viem'; import { encodeFunctionData, encodeFunctionResult, type AbiFunction } from 'viem';
import { getEnvValue } from 'configs/app/utils'; import { getEnvValue } from 'configs/app/utils';
...@@ -43,7 +43,7 @@ const fixture: TestFixture<MockContractReadResponseFixture, { page: Page }> = as ...@@ -43,7 +43,7 @@ const fixture: TestFixture<MockContractReadResponseFixture, { page: Page }> = as
value: params?.value, value: params?.value,
}; };
if (_isEqual(params, callParams) && id) { if (isEqual(params, callParams) && id) {
return route.fulfill({ return route.fulfill({
status: 200, status: 200,
json: { json: {
......
import type { TestFixture, Page } from '@playwright/test'; import type { TestFixture, Page } from '@playwright/test';
import _isEqual from 'lodash/isEqual'; import { isEqual } from 'es-toolkit';
import type { PublicRpcSchema } from 'viem'; import type { PublicRpcSchema } from 'viem';
import { getEnvValue } from 'configs/app/utils'; import { getEnvValue } from 'configs/app/utils';
...@@ -34,7 +34,7 @@ const fixture: TestFixture<MockRpcResponseFixture, { page: Page }> = async({ pag ...@@ -34,7 +34,7 @@ const fixture: TestFixture<MockRpcResponseFixture, { page: Page }> = async({ pag
...(rpcMock.Parameters ? { params: rpcMock.Parameters } : {}), ...(rpcMock.Parameters ? { params: rpcMock.Parameters } : {}),
}; };
if (_isEqual(json, payload) && id !== undefined) { if (isEqual(json, payload) && id !== undefined) {
return route.fulfill({ return route.fulfill({
status: 200, status: 200,
json: { json: {
......
import './fonts.css'; import './fonts.css';
import './index.css'; import './index.css';
import { beforeMount } from '@playwright/experimental-ct-react/hooks'; import { beforeMount } from '@playwright/experimental-ct-react/hooks';
import _defaultsDeep from 'lodash/defaultsDeep';
import MockDate from 'mockdate'; import MockDate from 'mockdate';
import * as router from 'next/router'; import * as router from 'next/router';
...@@ -12,12 +11,15 @@ const NEXT_ROUTER_MOCK = { ...@@ -12,12 +11,15 @@ const NEXT_ROUTER_MOCK = {
replace: () => Promise.resolve(), replace: () => Promise.resolve(),
}; };
beforeMount(async({ hooksConfig }) => { beforeMount(async({ hooksConfig }: { hooksConfig?: { router: typeof router } }) => {
// Before mount, redefine useRouter to return mock value from test. // Before mount, redefine useRouter to return mock value from test.
// @ts-ignore: I really want to redefine this property :) // @ts-ignore: I really want to redefine this property :)
// eslint-disable-next-line no-import-assign // eslint-disable-next-line no-import-assign
router.useRouter = () => _defaultsDeep(hooksConfig?.router, NEXT_ROUTER_MOCK); router.useRouter = () => ({
...NEXT_ROUTER_MOCK,
...hooksConfig?.router,
});
// set current date // set current date
MockDate.set('2022-11-11T12:00:00Z'); MockDate.set('2022-11-11T12:00:00Z');
......
This source diff could not be displayed because it is too large. You can view the blob instead.
This diff is collapsed.
...@@ -319,9 +319,9 @@ commander@^2.20.0: ...@@ -319,9 +319,9 @@ commander@^2.20.0:
integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
cross-spawn@^7.0.3: cross-spawn@^7.0.3:
version "7.0.3" version "7.0.6"
resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f"
integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==
dependencies: dependencies:
path-key "^3.1.0" path-key "^3.1.0"
shebang-command "^2.0.0" shebang-command "^2.0.0"
......
#!/bin/bash
# use this script for testing the og image generator
config_file="./configs/envs/.env.zkevm"
dotenv \
-e $config_file \
-- bash -c 'node ./deploy/scripts/og_image_generator.js'
\ No newline at end of file
#!/bin/bash
config_file="./configs/envs/.env.eth"
if [ ! -f "$config_file" ]; then
echo "Error: File '$config_file' not found."
exit 1
fi
dotenv \
-e $config_file \
-- bash -c 'cd ./deploy/tools/sitemap-generator && yarn && yarn next-sitemap'
\ No newline at end of file
...@@ -69,7 +69,7 @@ export type ZkEvmL2TxnBatch = { ...@@ -69,7 +69,7 @@ export type ZkEvmL2TxnBatch = {
export type ZkEvmL2TxnBatchTxs = { export type ZkEvmL2TxnBatchTxs = {
items: Array<Transaction>; items: Array<Transaction>;
// API responce doesn't have next_page_params option, but we need to add it to the type for consistency // API response doesn't have next_page_params option, but we need to add it to the type for consistency
next_page_params: null; next_page_params: null;
}; };
......
import noop from 'lodash/noop'; import { noop } from 'es-toolkit';
import React from 'react'; import React from 'react';
import { test, expect } from 'playwright/lib'; import { test, expect } from 'playwright/lib';
......
import { Accordion, Box, Flex, Link } from '@chakra-ui/react'; import { Accordion, Box, Flex, Link } from '@chakra-ui/react';
import _range from 'lodash/range'; import { range } from 'es-toolkit';
import React from 'react'; import React from 'react';
import type { SmartContractMethod } from './types'; import type { SmartContractMethod } from './types';
...@@ -39,7 +39,7 @@ const ContractAbi = ({ abi, addressHash, sourceAddress, tab, visibleItems }: Pro ...@@ -39,7 +39,7 @@ const ContractAbi = ({ abi, addressHash, sourceAddress, tab, visibleItems }: Pro
} }
if (expandedSections.length < abi.length) { if (expandedSections.length < abi.length) {
setExpandedSections(_range(0, abi.length)); setExpandedSections(range(0, abi.length));
} else { } else {
setExpandedSections([]); setExpandedSections([]);
} }
......
...@@ -28,7 +28,9 @@ interface Props { ...@@ -28,7 +28,9 @@ interface Props {
} }
const ContractMethodFieldInput = ({ data, hideLabel, path: name, className, isDisabled, isOptional: isOptionalProp, level }: Props) => { const ContractMethodFieldInput = ({ data, hideLabel, path: name, className, isDisabled, isOptional: isOptionalProp, level }: Props) => {
const ref = React.useRef<HTMLInputElement>(null); const ref = React.useRef<HTMLInputElement>();
const [ intPower, setIntPower ] = React.useState<number>(18);
const isNativeCoin = data.fieldType === 'native_coin'; const isNativeCoin = data.fieldType === 'native_coin';
const isOptional = isOptionalProp || isNativeCoin; const isOptional = isOptionalProp || isNativeCoin;
...@@ -46,6 +48,8 @@ const ContractMethodFieldInput = ({ data, hideLabel, path: name, className, isDi ...@@ -46,6 +48,8 @@ const ContractMethodFieldInput = ({ data, hideLabel, path: name, className, isDi
const hasMultiplyButton = argTypeMatchInt && Number(argTypeMatchInt.power) >= 64; const hasMultiplyButton = argTypeMatchInt && Number(argTypeMatchInt.power) >= 64;
React.useImperativeHandle(field.ref, () => ref.current);
const handleChange = React.useCallback((event: React.ChangeEvent<HTMLInputElement>) => { const handleChange = React.useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
const formattedValue = format(event.target.value); const formattedValue = format(event.target.value);
field.onChange(formattedValue); // data send back to hook form field.onChange(formattedValue); // data send back to hook form
...@@ -83,6 +87,42 @@ const ContractMethodFieldInput = ({ data, hideLabel, path: name, className, isDi ...@@ -83,6 +87,42 @@ const ContractMethodFieldInput = ({ data, hideLabel, path: name, className, isDi
setValue(name, newValue, { shouldValidate: true }); setValue(name, newValue, { shouldValidate: true });
}, [ format, name, setValue ]); }, [ format, name, setValue ]);
const handlePaste = React.useCallback((event: React.ClipboardEvent<HTMLInputElement>) => {
if (!argTypeMatchInt || !hasMultiplyButton) {
return;
}
const value = Number(event.clipboardData.getData('text'));
if (Object.is(value, NaN)) {
return;
}
const isFloat = Number.isFinite(value) && !Number.isInteger(value);
if (!isFloat) {
return;
}
event.preventDefault();
if (field.value) {
return;
}
const newValue = value * 10 ** intPower;
const formattedValue = format(newValue.toString());
field.onChange(formattedValue);
setValue(name, formattedValue, { shouldValidate: true });
window.setTimeout(() => {
// move cursor to the end of the input
// but we have to wait for the input to get the new value
const END_OF_INPUT = 100;
ref.current?.setSelectionRange(END_OF_INPUT, END_OF_INPUT);
}, 100);
}, [ argTypeMatchInt, hasMultiplyButton, intPower, format, field, setValue, name ]);
const error = fieldState.error; const error = fieldState.error;
return ( return (
...@@ -107,9 +147,14 @@ const ContractMethodFieldInput = ({ data, hideLabel, path: name, className, isDi ...@@ -107,9 +147,14 @@ const ContractMethodFieldInput = ({ data, hideLabel, path: name, className, isDi
thousandSeparator: ' ', thousandSeparator: ' ',
decimalScale: 0, decimalScale: 0,
allowNegative: !argTypeMatchInt.isUnsigned, allowNegative: !argTypeMatchInt.isUnsigned,
getInputRef: (element: HTMLInputElement) => {
ref.current = element;
},
} : {}) } } : {}) }
ref={ ref } // as we use mutable ref, we have to cast it to React.LegacyRef<HTMLInputElement> to trick chakra and typescript
ref={ ref as React.LegacyRef<HTMLInputElement> | undefined }
onChange={ handleChange } onChange={ handleChange }
onPaste={ handlePaste }
required={ !isOptional } required={ !isOptional }
isInvalid={ Boolean(error) } isInvalid={ Boolean(error) }
placeholder={ data.type } placeholder={ data.type }
...@@ -148,7 +193,14 @@ const ContractMethodFieldInput = ({ data, hideLabel, path: name, className, isDi ...@@ -148,7 +193,14 @@ const ContractMethodFieldInput = ({ data, hideLabel, path: name, className, isDi
Max Max
</Button> </Button>
)) } )) }
{ hasMultiplyButton && <ContractMethodMultiplyButton onClick={ handleMultiplyButtonClick } isDisabled={ isDisabled }/> } { hasMultiplyButton && (
<ContractMethodMultiplyButton
onClick={ handleMultiplyButtonClick }
isDisabled={ isDisabled }
initialValue={ intPower }
onChange={ setIntPower }
/>
) }
</InputRightElement> </InputRightElement>
</InputGroup> </InputGroup>
{ error && <Box color="error" fontSize="sm" lineHeight={ 5 } mt={ 1 }>{ error.message }</Box> } { error && <Box color="error" fontSize="sm" lineHeight={ 5 } mt={ 1 }>{ error.message }</Box> }
......
...@@ -20,10 +20,12 @@ import IconSvg from 'ui/shared/IconSvg'; ...@@ -20,10 +20,12 @@ import IconSvg from 'ui/shared/IconSvg';
interface Props { interface Props {
onClick: (power: number) => void; onClick: (power: number) => void;
isDisabled?: boolean; isDisabled?: boolean;
initialValue: number;
onChange: (power: number) => void;
} }
const ContractMethodMultiplyButton = ({ onClick, isDisabled }: Props) => { const ContractMethodMultiplyButton = ({ onClick, isDisabled, initialValue, onChange }: Props) => {
const [ selectedOption, setSelectedOption ] = React.useState<number | undefined>(18); const [ selectedOption, setSelectedOption ] = React.useState<number | undefined>(initialValue);
const [ customValue, setCustomValue ] = React.useState<number>(); const [ customValue, setCustomValue ] = React.useState<number>();
const { isOpen, onToggle, onClose } = useDisclosure(); const { isOpen, onToggle, onClose } = useDisclosure();
...@@ -35,13 +37,16 @@ const ContractMethodMultiplyButton = ({ onClick, isDisabled }: Props) => { ...@@ -35,13 +37,16 @@ const ContractMethodMultiplyButton = ({ onClick, isDisabled }: Props) => {
setSelectedOption((prev) => prev === id ? undefined : id); setSelectedOption((prev) => prev === id ? undefined : id);
setCustomValue(undefined); setCustomValue(undefined);
onClose(); onClose();
onChange(id);
} }
}, [ onClose ]); }, [ onClose, onChange ]);
const handleInputChange = React.useCallback((event: React.ChangeEvent<HTMLInputElement>) => { const handleInputChange = React.useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
setCustomValue(Number(event.target.value)); const value = Number(event.target.value);
setCustomValue(value);
setSelectedOption(undefined); setSelectedOption(undefined);
}, []); onChange(value);
}, [ onChange ]);
const value = selectedOption || customValue; const value = selectedOption || customValue;
......
import _set from 'lodash/set'; import { set } from 'es-toolkit/compat';
import type { ContractAbiItemInput } from '../types'; import type { ContractAbiItemInput } from '../types';
...@@ -78,7 +78,7 @@ export function transformFormDataToMethodArgs(formData: ContractMethodFormFields ...@@ -78,7 +78,7 @@ export function transformFormDataToMethodArgs(formData: ContractMethodFormFields
for (const field in formData) { for (const field in formData) {
const value = formData[field]; const value = formData[field];
_set(result, field.replaceAll(':', '.'), value); set(result, field.replaceAll(':', '.'), value);
} }
const filteredResult = filterOutEmptyItems(result); const filteredResult = filterOutEmptyItems(result);
......
import _pickBy from 'lodash/pickBy'; import { pickBy } from 'es-toolkit';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
...@@ -63,7 +63,7 @@ export default function useMethodsFilters({ abi }: Params) { ...@@ -63,7 +63,7 @@ export default function useMethodsFilters({ abi }: Params) {
return; return;
} }
const queryForPathname = _pickBy(router.query, (value, key) => router.pathname.includes(`[${ key }]`)); const queryForPathname = pickBy(router.query, (value, key) => router.pathname.includes(`[${ key }]`));
router.push( router.push(
{ pathname: router.pathname, query: { ...queryForPathname, tab: nextTab } }, { pathname: router.pathname, query: { ...queryForPathname, tab: nextTab } },
undefined, undefined,
......
import { Image, Tooltip } from '@chakra-ui/react'; import { Image, Tooltip } from '@chakra-ui/react';
import _capitalize from 'lodash/capitalize'; import { capitalize } from 'es-toolkit';
import React from 'react'; import React from 'react';
import type { MultichainProviderConfigParsed } from 'types/client/multichainProviderConfig'; import type { MultichainProviderConfigParsed } from 'types/client/multichainProviderConfig';
...@@ -25,10 +25,10 @@ const AddressMultichainButton = ({ item, addressHash, onClick, hasSingleProvider ...@@ -25,10 +25,10 @@ const AddressMultichainButton = ({ item, addressHash, onClick, hasSingleProvider
const buttonContent = hasSingleProvider ? ( const buttonContent = hasSingleProvider ? (
<> <>
{ buttonIcon } { buttonIcon }
{ _capitalize(item.name) } { capitalize(item.name) }
</> </>
) : ( ) : (
<Tooltip label={ _capitalize(item.name) }>{ buttonIcon }</Tooltip> <Tooltip label={ capitalize(item.name) }>{ buttonIcon }</Tooltip>
); );
const linkProps = { const linkProps = {
......
...@@ -12,7 +12,7 @@ import { ...@@ -12,7 +12,7 @@ import {
chakra, chakra,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query'; import type { UseQueryResult } from '@tanstack/react-query';
import _clamp from 'lodash/clamp'; import { clamp } from 'es-toolkit';
import React from 'react'; import React from 'react';
import type * as bens from '@blockscout/bens-types'; import type * as bens from '@blockscout/bens-types';
...@@ -37,7 +37,7 @@ interface Props { ...@@ -37,7 +37,7 @@ interface Props {
const DomainsGrid = ({ data }: { data: Array<bens.Domain> }) => { const DomainsGrid = ({ data }: { data: Array<bens.Domain> }) => {
return ( return (
<Grid <Grid
templateColumns={{ base: `repeat(${ _clamp(data.length, 1, 2) }, 1fr)`, lg: `repeat(${ _clamp(data.length, 1, 3) }, 1fr)` }} templateColumns={{ base: `repeat(${ clamp(data.length, 1, 2) }, 1fr)`, lg: `repeat(${ clamp(data.length, 1, 3) }, 1fr)` }}
columnGap={ 8 } columnGap={ 8 }
rowGap={ 4 } rowGap={ 4 }
mt={ 2 } mt={ 2 }
......
import { Box, Flex, IconButton, Tooltip } from '@chakra-ui/react'; import { Box, Flex, IconButton, Tooltip } from '@chakra-ui/react';
import { useQueryClient, useIsFetching } from '@tanstack/react-query'; import { useQueryClient, useIsFetching } from '@tanstack/react-query';
import _sumBy from 'lodash/sumBy'; import { sumBy } from 'es-toolkit';
import NextLink from 'next/link'; import NextLink from 'next/link';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
...@@ -50,7 +50,7 @@ const TokenSelect = ({ onClick }: Props) => { ...@@ -50,7 +50,7 @@ const TokenSelect = ({ onClick }: Props) => {
); );
} }
const hasTokens = _sumBy(Object.values(data), ({ items }) => items.length) > 0; const hasTokens = sumBy(Object.values(data), ({ items }) => items.length) > 0;
if (isError || !hasTokens) { if (isError || !hasTokens) {
return <Box py="6px">0</Box>; return <Box py="6px">0</Box>;
} }
......
import { Text, Box, Input, InputGroup, InputLeftElement, useColorModeValue, Flex, Link } from '@chakra-ui/react'; import { Text, Box, Input, InputGroup, InputLeftElement, useColorModeValue, Flex, Link } from '@chakra-ui/react';
import _sumBy from 'lodash/sumBy'; import { sumBy } from 'es-toolkit';
import type { ChangeEvent } from 'react'; import type { ChangeEvent } from 'react';
import React from 'react'; import React from 'react';
...@@ -26,7 +26,7 @@ interface Props { ...@@ -26,7 +26,7 @@ interface Props {
const TokenSelectMenu = ({ erc20sort, erc1155sort, erc404sort, filteredData, onInputChange, onSortClick, searchTerm }: Props) => { const TokenSelectMenu = ({ erc20sort, erc1155sort, erc404sort, filteredData, onInputChange, onSortClick, searchTerm }: Props) => {
const searchIconColor = useColorModeValue('blackAlpha.600', 'whiteAlpha.600'); const searchIconColor = useColorModeValue('blackAlpha.600', 'whiteAlpha.600');
const hasFilteredResult = _sumBy(Object.values(filteredData), ({ items }) => items.length) > 0; const hasFilteredResult = sumBy(Object.values(filteredData), ({ items }) => items.length) > 0;
return ( return (
<> <>
......
import _mapValues from 'lodash/mapValues'; import { mapValues } from 'es-toolkit';
import type { ChangeEvent } from 'react'; import type { ChangeEvent } from 'react';
import React from 'react'; import React from 'react';
...@@ -31,7 +31,7 @@ export default function useTokenSelect(data: FormattedData) { ...@@ -31,7 +31,7 @@ export default function useTokenSelect(data: FormattedData) {
}, []); }, []);
const filteredData = React.useMemo(() => { const filteredData = React.useMemo(() => {
return _mapValues(data, ({ items, isOverflow }) => ({ return mapValues(data, ({ items, isOverflow }) => ({
isOverflow, isOverflow,
items: items.filter(filterTokens(searchTerm.toLowerCase())), items: items.filter(filterTokens(searchTerm.toLowerCase())),
})); }));
......
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import fpAdd from 'lodash/fp/add';
import type { AddressTokenBalance } from 'types/api/address'; import type { AddressTokenBalance } from 'types/api/address';
import type { TokenType } from 'types/api/token'; import type { TokenType } from 'types/api/token';
...@@ -100,7 +99,7 @@ export const getTokensTotalInfo = (data: TokenSelectData) => { ...@@ -100,7 +99,7 @@ export const getTokensTotalInfo = (data: TokenSelectData) => {
const num = Object.values(data) const num = Object.values(data)
.map(({ items }) => items.length) .map(({ items }) => items.length)
.reduce(fpAdd, 0); .reduce((result, item) => result + item, 0);
const isOverflow = Object.values(data).some(({ isOverflow }) => isOverflow); const isOverflow = Object.values(data).some(({ isOverflow }) => isOverflow);
......
...@@ -118,7 +118,9 @@ export default function useAddressQuery({ hash, isEnabled = true }: Params): Add ...@@ -118,7 +118,9 @@ export default function useAddressQuery({ hash, isEnabled = true }: Params): Add
}, [ rpcQuery.data, rpcQuery.isPlaceholderData ]); }, [ rpcQuery.data, rpcQuery.isPlaceholderData ]);
const isRpcQuery = Boolean( const isRpcQuery = Boolean(
(apiQuery.isError || apiQuery.isPlaceholderData) && (apiQuery.isError || apiQuery.isPlaceholderData) &&
!(apiQuery.error?.status && NO_RPC_FALLBACK_ERROR_CODES.includes(apiQuery.error?.status)) &&
!NO_RPC_FALLBACK_ERROR_CODES.includes(apiQuery.error?.status ?? 999) && !NO_RPC_FALLBACK_ERROR_CODES.includes(apiQuery.error?.status ?? 999) &&
apiQuery.errorUpdateCount > 0 && apiQuery.errorUpdateCount > 0 &&
rpcQuery.data && rpcQuery.data &&
......
import { Flex, Select, Input, InputGroup, InputRightElement, VStack, IconButton } from '@chakra-ui/react'; import { Flex, Select, Input, InputGroup, InputRightElement, VStack, IconButton } from '@chakra-ui/react';
import isEqual from 'lodash/isEqual'; import { isEqual } from 'es-toolkit';
import type { ChangeEvent } from 'react'; import type { ChangeEvent } from 'react';
import React from 'react'; import React from 'react';
......
import { Flex, Input, Text } from '@chakra-ui/react'; import { Flex, Input, Text } from '@chakra-ui/react';
import isEqual from 'lodash/isEqual'; import { isEqual } from 'es-toolkit';
import type { ChangeEvent } from 'react'; import type { ChangeEvent } from 'react';
import React from 'react'; import React from 'react';
......
import { Flex, Input, Tag, Text } from '@chakra-ui/react'; import { Flex, Input, Tag, Text } from '@chakra-ui/react';
import isEqual from 'lodash/isEqual'; import { isEqual } from 'es-toolkit';
import type { ChangeEvent } from 'react'; import type { ChangeEvent } from 'react';
import React from 'react'; import React from 'react';
......
import { Flex, Checkbox, CheckboxGroup, Text, Spinner, Select } from '@chakra-ui/react'; import { Flex, Checkbox, CheckboxGroup, Text, Spinner, Select } from '@chakra-ui/react';
import isEqual from 'lodash/isEqual'; import { isEqual } from 'es-toolkit';
import React from 'react'; import React from 'react';
import type { AdvancedFilterParams } from 'types/api/advancedFilter'; import type { AdvancedFilterParams } from 'types/api/advancedFilter';
......
import { Flex, Checkbox, CheckboxGroup, Spinner, chakra } from '@chakra-ui/react'; import { Flex, Checkbox, CheckboxGroup, Spinner, chakra } from '@chakra-ui/react';
import differenceBy from 'lodash/differenceBy'; import { isEqual, differenceBy } from 'es-toolkit';
import isEqual from 'lodash/isEqual';
import type { ChangeEvent } from 'react'; import type { ChangeEvent } from 'react';
import React from 'react'; import React from 'react';
......
import { Flex, Checkbox, CheckboxGroup } from '@chakra-ui/react'; import { Flex, Checkbox, CheckboxGroup } from '@chakra-ui/react';
import isEqual from 'lodash/isEqual'; import { isEqual, without } from 'es-toolkit';
import without from 'lodash/without';
import type { ChangeEvent } from 'react'; import type { ChangeEvent } from 'react';
import React from 'react'; import React from 'react';
......
import castArray from 'lodash/castArray'; import { castArray } from 'es-toolkit/compat';
import type { AdvancedFilterAge, AdvancedFilterParams } from 'types/api/advancedFilter'; import type { AdvancedFilterAge, AdvancedFilterParams } from 'types/api/advancedFilter';
......
import { Grid, GridItem, Text, Link, Box, Tooltip } from '@chakra-ui/react'; import { Grid, GridItem, Text, Link, Box, Tooltip } from '@chakra-ui/react';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import capitalize from 'lodash/capitalize'; import { capitalize } from 'es-toolkit';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import { scroller, Element } from 'react-scroll'; import { scroller, Element } from 'react-scroll';
......
import _padStart from 'lodash/padStart'; import { padStart } from 'es-toolkit/compat';
export default function splitSecondsInPeriods(value: number) { export default function splitSecondsInPeriods(value: number) {
const seconds = value % 60; const seconds = value % 60;
...@@ -7,9 +7,9 @@ export default function splitSecondsInPeriods(value: number) { ...@@ -7,9 +7,9 @@ export default function splitSecondsInPeriods(value: number) {
const days = (value - seconds - minutes * 60 - hours * 60 * 60) / (60 * 60 * 24); const days = (value - seconds - minutes * 60 - hours * 60 * 60) / (60 * 60 * 24);
return { return {
seconds: _padStart(String(seconds), 2, '0'), seconds: padStart(String(seconds), 2, '0'),
minutes: _padStart(String(minutes), 2, '0'), minutes: padStart(String(minutes), 2, '0'),
hours: _padStart(String(hours), 2, '0'), hours: padStart(String(hours), 2, '0'),
days: _padStart(String(days), 2, '0'), days: padStart(String(days), 2, '0'),
}; };
} }
import { Flex, Text, Box, Tooltip } from '@chakra-ui/react'; import { Flex, Text, Box, Tooltip } from '@chakra-ui/react';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import capitalize from 'lodash/capitalize'; import { capitalize } from 'es-toolkit';
import React from 'react'; import React from 'react';
import type { Block } from 'types/api/block'; import type { Block } from 'types/api/block';
......
import { Table, Tbody, Tr, Th } from '@chakra-ui/react'; import { Table, Tbody, Tr, Th } from '@chakra-ui/react';
import { capitalize } from 'es-toolkit';
import { AnimatePresence } from 'framer-motion'; import { AnimatePresence } from 'framer-motion';
import capitalize from 'lodash/capitalize';
import React from 'react'; import React from 'react';
import type { Block } from 'types/api/block'; import type { Block } from 'types/api/block';
......
import _get from 'lodash/get'; import { get } from 'es-toolkit/compat';
import React from 'react'; import React from 'react';
import { useFormContext } from 'react-hook-form'; import { useFormContext } from 'react-hook-form';
...@@ -43,7 +43,7 @@ const ContractVerificationFieldGitHubRepo = ({ onCommitHashChange }: Props) => { ...@@ -43,7 +43,7 @@ const ContractVerificationFieldGitHubRepo = ({ onCommitHashChange }: Props) => {
const response = await fetch(`https://api.github.com/repos/${ gitHubData.owner }/${ gitHubData.repo }/commits?per_page=1`); const response = await fetch(`https://api.github.com/repos/${ gitHubData.owner }/${ gitHubData.repo }/commits?per_page=1`);
repoErrorRef.current = undefined; repoErrorRef.current = undefined;
trigger('repository_url'); trigger('repository_url');
onCommitHashChange(_get(response, '[0].sha')); onCommitHashChange(get(response, '[0].sha'));
return; return;
} catch (error) { } catch (error) {
repoErrorRef.current = 'GitHub repository not found'; repoErrorRef.current = 'GitHub repository not found';
......
...@@ -15,7 +15,7 @@ const ContractVerificationSolidityFoundry = () => { ...@@ -15,7 +15,7 @@ const ContractVerificationSolidityFoundry = () => {
const address = watch('address'); const address = watch('address');
const codeSnippet = `forge verify-contract \\ const codeSnippet = `forge verify-contract \\
--rpc-url ${ config.chain.rpcUrl || `${ config.api.endpoint }/api/eth-rpc` } \\ --rpc-url ${ config.chain.rpcUrls[0] || `${ config.api.endpoint }/api/eth-rpc` } \\
--verifier blockscout \\ --verifier blockscout \\
--verifier-url '${ config.api.endpoint }/api/' \\ --verifier-url '${ config.api.endpoint }/api/' \\
${ address || '<address>' } \\ ${ address || '<address>' } \\
......
...@@ -22,7 +22,7 @@ const ContractVerificationSolidityHardhat = ({ config: formConfig }: { config: S ...@@ -22,7 +22,7 @@ const ContractVerificationSolidityHardhat = ({ config: formConfig }: { config: S
solidity: "${ latestSolidityVersion || '0.8.24' }", // replace if necessary solidity: "${ latestSolidityVersion || '0.8.24' }", // replace if necessary
networks: { networks: {
'${ chainNameSlug }': { '${ chainNameSlug }': {
url: '${ config.chain.rpcUrl || `${ config.api.endpoint }/api/eth-rpc` }' url: '${ config.chain.rpcUrls[0] || `${ config.api.endpoint }/api/eth-rpc` }'
}, },
}, },
etherscan: { etherscan: {
......
import _capitalize from 'lodash/capitalize'; import { capitalize } from 'es-toolkit';
import React from 'react'; import React from 'react';
import type { UseFormReturn } from 'react-hook-form'; import type { UseFormReturn } from 'react-hook-form';
...@@ -40,7 +40,7 @@ const CsvExportFormField = ({ formApi, name }: Props) => { ...@@ -40,7 +40,7 @@ const CsvExportFormField = ({ formApi, name }: Props) => {
name={ name } name={ name }
type="date" type="date"
max={ dayjs().format('YYYY-MM-DD') } max={ dayjs().format('YYYY-MM-DD') }
placeholder={ _capitalize(name) } placeholder={ capitalize(name) }
isRequired isRequired
rules={{ validate }} rules={{ validate }}
size={{ base: 'md', lg: 'lg' }} size={{ base: 'md', lg: 'lg' }}
......
...@@ -51,7 +51,7 @@ test('degradation view', async({ render, page, mockRpcResponse, mockApiResponse ...@@ -51,7 +51,7 @@ test('degradation view', async({ render, page, mockRpcResponse, mockApiResponse
}); });
const component = await render(<Address/>, { hooksConfig }); const component = await render(<Address/>, { hooksConfig });
await page.waitForResponse(config.chain.rpcUrl as string); await page.waitForResponse(config.chain.rpcUrls[0]);
await expect(component).toHaveScreenshot({ await expect(component).toHaveScreenshot({
mask: [ page.locator(pwConfig.adsBannerSelector) ], mask: [ page.locator(pwConfig.adsBannerSelector) ],
......
...@@ -69,7 +69,6 @@ const AddressPageContent = () => { ...@@ -69,7 +69,6 @@ const AddressPageContent = () => {
const tabsScrollRef = React.useRef<HTMLDivElement>(null); const tabsScrollRef = React.useRef<HTMLDivElement>(null);
const hash = getQueryParamString(router.query.hash); const hash = getQueryParamString(router.query.hash);
const checkSummedHash = React.useMemo(() => getCheckedSummedAddress(hash), [ hash ]);
const checkDomainName = useCheckDomainNameParam(hash); const checkDomainName = useCheckDomainNameParam(hash);
const checkAddressFormat = useCheckAddressFormat(hash); const checkAddressFormat = useCheckAddressFormat(hash);
...@@ -364,6 +363,10 @@ const AddressPageContent = () => { ...@@ -364,6 +363,10 @@ const AddressPageContent = () => {
return; return;
}, [ appProps.referrer ]); }, [ appProps.referrer ]);
// API always returns hash in check-summed format except for addresses that are not in the database
// In this case it returns 404 with empty payload, so we calculate check-summed hash on the client
const checkSummedHash = React.useMemo(() => addressQuery.data?.hash ?? getCheckedSummedAddress(hash), [ hash, addressQuery.data?.hash ]);
const titleSecondRow = ( const titleSecondRow = (
<Flex alignItems="center" w="100%" columnGap={ 2 } rowGap={ 2 } flexWrap={{ base: 'wrap', lg: 'nowrap' }}> <Flex alignItems="center" w="100%" columnGap={ 2 } rowGap={ 2 } flexWrap={{ base: 'wrap', lg: 'nowrap' }}>
{ addressQuery.data?.ens_domain_name && ( { addressQuery.data?.ens_domain_name && (
......
...@@ -15,7 +15,7 @@ import { ...@@ -15,7 +15,7 @@ import {
HStack, HStack,
Link, Link,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import omit from 'lodash/omit'; import { omit } from 'es-toolkit';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
......
...@@ -28,7 +28,7 @@ test('degradation view, details tab', async({ render, mockApiResponse, mockRpcRe ...@@ -28,7 +28,7 @@ test('degradation view, details tab', async({ render, mockApiResponse, mockRpcRe
}); });
const component = await render(<Block/>, { hooksConfig }); const component = await render(<Block/>, { hooksConfig });
await page.waitForResponse(config.chain.rpcUrl as string); await page.waitForResponse(config.chain.rpcUrls[0]);
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
...@@ -49,7 +49,7 @@ test('degradation view, txs tab', async({ render, mockApiResponse, mockRpcRespon ...@@ -49,7 +49,7 @@ test('degradation view, txs tab', async({ render, mockApiResponse, mockRpcRespon
}); });
const component = await render(<Block/>, { hooksConfig }); const component = await render(<Block/>, { hooksConfig });
await page.waitForResponse(config.chain.rpcUrl as string); await page.waitForResponse(config.chain.rpcUrls[0]);
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
...@@ -71,7 +71,7 @@ test('degradation view, withdrawals tab', async({ render, mockApiResponse, mockR ...@@ -71,7 +71,7 @@ test('degradation view, withdrawals tab', async({ render, mockApiResponse, mockR
}); });
const component = await render(<Block/>, { hooksConfig }); const component = await render(<Block/>, { hooksConfig });
await page.waitForResponse(config.chain.rpcUrl as string); await page.waitForResponse(config.chain.rpcUrls[0]);
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
import { chakra } from '@chakra-ui/react'; import { chakra } from '@chakra-ui/react';
import capitalize from 'lodash/capitalize'; import { capitalize } from 'es-toolkit';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
......
...@@ -65,7 +65,7 @@ const MarketplaceAppContent = ({ address, data, isPending, appUrl }: Props) => { ...@@ -65,7 +65,7 @@ const MarketplaceAppContent = ({ address, data, isPending, appUrl }: Props) => {
blockscoutNetworkName: config.chain.name, blockscoutNetworkName: config.chain.name,
blockscoutNetworkId: Number(config.chain.id), blockscoutNetworkId: Number(config.chain.id),
blockscoutNetworkCurrency: config.chain.currency, blockscoutNetworkCurrency: config.chain.currency,
blockscoutNetworkRpc: config.chain.rpcUrl, blockscoutNetworkRpc: config.chain.rpcUrls[0],
}; };
iframeRef?.current?.contentWindow?.postMessage(message, data.url); iframeRef?.current?.contentWindow?.postMessage(message, data.url);
...@@ -159,7 +159,7 @@ const MarketplaceApp = () => { ...@@ -159,7 +159,7 @@ const MarketplaceApp = () => {
<DappscoutIframeProvider <DappscoutIframeProvider
address={ address } address={ address }
appUrl={ appUrl } appUrl={ appUrl }
rpcUrl={ config.chain.rpcUrl } rpcUrl={ config.chain.rpcUrls[0] }
sendTransaction={ sendTransaction } sendTransaction={ sendTransaction }
signMessage={ signMessage } signMessage={ signMessage }
signTypedData={ signTypedData } signTypedData={ signTypedData }
......
...@@ -7,24 +7,40 @@ import { test, expect } from 'playwright/lib'; ...@@ -7,24 +7,40 @@ import { test, expect } from 'playwright/lib';
import Tokens from './Tokens'; import Tokens from './Tokens';
test.beforeEach(async({ mockTextAd }) => { test.beforeEach(async({ mockTextAd, mockAssetResponse }) => {
await mockTextAd(); await mockTextAd();
await mockAssetResponse(tokens.tokenInfoERC20a.icon_url as string, './playwright/mocks/image_svg.svg');
}); });
const allTokens = {
items: [
tokens.tokenInfoERC20a, tokens.tokenInfoERC20b, tokens.tokenInfoERC20c, tokens.tokenInfoERC20d,
tokens.tokenInfoERC721a, tokens.tokenInfoERC721b, tokens.tokenInfoERC721c,
tokens.tokenInfoERC1155a, tokens.tokenInfoERC1155b, tokens.tokenInfoERC1155WithoutName,
],
next_page_params: {
holder_count: 1,
items_count: 1,
name: 'a',
market_cap: '0',
},
};
test('base view +@mobile +@dark-mode', async({ render, mockApiResponse }) => { test('base view +@mobile +@dark-mode', async({ render, mockApiResponse }) => {
const allTokens = {
items: [ await mockApiResponse('tokens', allTokens);
tokens.tokenInfoERC20a, tokens.tokenInfoERC20b, tokens.tokenInfoERC20c, tokens.tokenInfoERC20d,
tokens.tokenInfoERC721a, tokens.tokenInfoERC721b, tokens.tokenInfoERC721c, const component = await render(
tokens.tokenInfoERC1155a, tokens.tokenInfoERC1155b, tokens.tokenInfoERC1155WithoutName, <div>
], <Box h={{ base: '134px', lg: 6 }}/>
next_page_params: { <Tokens/>
holder_count: 1, </div>,
items_count: 1, );
name: 'a',
market_cap: '0', await expect(component).toHaveScreenshot();
}, });
};
test('with search +@mobile +@dark-mode', async({ render, mockApiResponse }) => {
const filteredTokens = { const filteredTokens = {
items: [ items: [
tokens.tokenInfoERC20a, tokens.tokenInfoERC20b, tokens.tokenInfoERC20c, tokens.tokenInfoERC20a, tokens.tokenInfoERC20b, tokens.tokenInfoERC20c,
...@@ -42,10 +58,9 @@ test('base view +@mobile +@dark-mode', async({ render, mockApiResponse }) => { ...@@ -42,10 +58,9 @@ test('base view +@mobile +@dark-mode', async({ render, mockApiResponse }) => {
</div>, </div>,
); );
await expect(component).toHaveScreenshot();
await component.getByRole('textbox', { name: 'Token name or symbol' }).focus(); await component.getByRole('textbox', { name: 'Token name or symbol' }).focus();
await component.getByRole('textbox', { name: 'Token name or symbol' }).fill('foo'); await component.getByRole('textbox', { name: 'Token name or symbol' }).fill('foo');
await component.getByRole('textbox', { name: 'Token name or symbol' }).blur();
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
......
import { Flex } from '@chakra-ui/react'; import { Flex } from '@chakra-ui/react';
import capitalize from 'lodash/capitalize'; import { capitalize } from 'es-toolkit';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
......
import _inRange from 'lodash/inRange'; import { inRange } from 'es-toolkit';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
...@@ -43,7 +43,7 @@ const UserOp = () => { ...@@ -43,7 +43,7 @@ const UserOp = () => {
if (!userOpQuery.data) { if (!userOpQuery.data) {
return true; return true;
} else { } else {
if (_inRange( if (inRange(
Number(tt.log_index), Number(tt.log_index),
userOpQuery.data?.user_logs_start_index, userOpQuery.data?.user_logs_start_index,
userOpQuery.data?.user_logs_start_index + userOpQuery.data?.user_logs_count, userOpQuery.data?.user_logs_start_index + userOpQuery.data?.user_logs_count,
...@@ -58,7 +58,7 @@ const UserOp = () => { ...@@ -58,7 +58,7 @@ const UserOp = () => {
if (!userOpQuery.data) { if (!userOpQuery.data) {
return true; return true;
} else { } else {
if (_inRange(log.index, userOpQuery.data?.user_logs_start_index, userOpQuery.data?.user_logs_start_index + userOpQuery.data?.user_logs_count)) { if (inRange(log.index, userOpQuery.data?.user_logs_start_index, userOpQuery.data?.user_logs_start_index + userOpQuery.data?.user_logs_count)) {
return true; return true;
} }
return false; return false;
......
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