Commit e24a3f2d authored by tom's avatar tom

Merge branch 'main' of github.com:blockscout/frontend into tom2drum/rollbar-fix

parents fa257375 10881261
...@@ -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.
...@@ -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 && (
...@@ -406,7 +409,7 @@ const AddressPageContent = () => { ...@@ -406,7 +409,7 @@ const AddressPageContent = () => {
<SolidityscanReport hash={ hash }/> } <SolidityscanReport hash={ hash }/> }
{ !isLoading && addressEnsDomainsQuery.data && config.features.nameService.isEnabled && { !isLoading && addressEnsDomainsQuery.data && config.features.nameService.isEnabled &&
<AddressEnsDomains query={ addressEnsDomainsQuery } addressHash={ hash } mainDomainName={ addressQuery.data?.ens_domain_name }/> } <AddressEnsDomains query={ addressEnsDomainsQuery } addressHash={ hash } mainDomainName={ addressQuery.data?.ens_domain_name }/> }
<NetworkExplorers type="address" pathParam={ hash }/> <NetworkExplorers type="address" pathParam={ hash.toLowerCase() }/>
</Flex> </Flex>
); );
......
...@@ -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;
......
import { Alert, Box, Button, Flex, Grid, GridItem } from '@chakra-ui/react'; import { Alert, Box, Button, Flex, Grid, GridItem } from '@chakra-ui/react';
import _pickBy from 'lodash/pickBy'; import { pickBy } from 'es-toolkit';
import React from 'react'; import React from 'react';
import type { FormSubmitResult } from './types'; import type { FormSubmitResult } from './types';
...@@ -26,7 +26,7 @@ const PublicTagsSubmitResult = ({ data }: Props) => { ...@@ -26,7 +26,7 @@ const PublicTagsSubmitResult = ({ data }: Props) => {
const hasErrors = groupedData.items.some((item) => item.error !== null); const hasErrors = groupedData.items.some((item) => item.error !== null);
const companyWebsite = makePrettyLink(groupedData.companyWebsite); const companyWebsite = makePrettyLink(groupedData.companyWebsite);
const startOverButtonQuery = hasErrors ? _pickBy({ const startOverButtonQuery = hasErrors ? pickBy({
requesterName: groupedData.requesterName, requesterName: groupedData.requesterName,
requesterEmail: groupedData.requesterEmail, requesterEmail: groupedData.requesterEmail,
companyName: groupedData.companyName, companyName: groupedData.companyName,
......
import { chakra, Flex } from '@chakra-ui/react'; import { chakra, Flex } from '@chakra-ui/react';
import type { GroupBase, SelectComponentsConfig, SingleValueProps } from 'chakra-react-select'; import type { GroupBase, SelectComponentsConfig, SingleValueProps } from 'chakra-react-select';
import { chakraComponents } from 'chakra-react-select'; import { chakraComponents } from 'chakra-react-select';
import _capitalize from 'lodash/capitalize'; import { capitalize } from 'es-toolkit';
import React from 'react'; import React from 'react';
import { useFormContext } from 'react-hook-form'; import { useFormContext } from 'react-hook-form';
...@@ -22,7 +22,7 @@ const PublicTagsSubmitFieldTagType = ({ index, tagTypes }: Props) => { ...@@ -22,7 +22,7 @@ const PublicTagsSubmitFieldTagType = ({ index, tagTypes }: Props) => {
const typeOptions = React.useMemo(() => tagTypes?.map((type) => ({ const typeOptions = React.useMemo(() => tagTypes?.map((type) => ({
value: type.type, value: type.type,
label: _capitalize(type.type), label: capitalize(type.type),
})) ?? [], [ tagTypes ]); })) ?? [], [ tagTypes ]);
const fieldValue = watch(`tags.${ index }.type`).value; const fieldValue = watch(`tags.${ index }.type`).value;
......
import { Box, Button, Flex, Grid, GridItem, useColorModeValue } from '@chakra-ui/react'; import { Box, Button, Flex, Grid, GridItem, useColorModeValue } from '@chakra-ui/react';
import _pickBy from 'lodash/pickBy'; import { pickBy } from 'es-toolkit';
import React from 'react'; import React from 'react';
import type { FormSubmitResultGrouped } from '../types'; import type { FormSubmitResultGrouped } from '../types';
...@@ -23,7 +23,7 @@ const PublicTagsSubmitResultWithErrors = ({ data }: Props) => { ...@@ -23,7 +23,7 @@ const PublicTagsSubmitResultWithErrors = ({ data }: Props) => {
<Flex flexDir="column" rowGap={ 3 }> <Flex flexDir="column" rowGap={ 3 }>
{ data.items.map((item, index) => { { data.items.map((item, index) => {
const startOverButtonQuery = _pickBy({ const startOverButtonQuery = pickBy({
addresses: item.addresses, addresses: item.addresses,
requesterName: data.requesterName, requesterName: data.requesterName,
requesterEmail: data.requesterEmail, requesterEmail: data.requesterEmail,
......
import _isEqual from 'lodash/isEqual'; import { pickBy, isEqual } from 'es-toolkit';
import _pickBy from 'lodash/pickBy';
import type { FormFieldTag, FormFields, FormSubmitResult, FormSubmitResultGrouped, FormSubmitResultItemGrouped, SubmitRequestBody } from './types'; import type { FormFieldTag, FormFields, FormSubmitResult, FormSubmitResultGrouped, FormSubmitResultItemGrouped, SubmitRequestBody } from './types';
import type { UserInfo } from 'types/api/account'; import type { UserInfo } from 'types/api/account';
...@@ -22,7 +21,7 @@ export function convertFormDataToRequestsBody(data: FormFields): Array<SubmitReq ...@@ -22,7 +21,7 @@ export function convertFormDataToRequestsBody(data: FormFields): Array<SubmitReq
name: tag.name, name: tag.name,
tagType: tag.type.value, tagType: tag.type.value,
description: data.description, description: data.description,
meta: _pickBy({ meta: pickBy({
bgColor: tag.bgColor, bgColor: tag.bgColor,
textColor: tag.textColor, textColor: tag.textColor,
tagUrl: tag.url, tagUrl: tag.url,
...@@ -72,7 +71,7 @@ export function groupSubmitResult(data: FormSubmitResult | undefined): FormSubmi ...@@ -72,7 +71,7 @@ export function groupSubmitResult(data: FormSubmitResult | undefined): FormSubmi
// merge items with the same error and tags // merge items with the same error and tags
for (const item of _items) { for (const item of _items) {
const existingItem = items.find(({ error, tags }) => error === item.error && _isEqual(tags, item.tags)); const existingItem = items.find(({ error, tags }) => error === item.error && isEqual(tags, item.tags));
if (existingItem) { if (existingItem) {
existingItem.addresses.push(...item.addresses); existingItem.addresses.push(...item.addresses);
continue; continue;
......
import { PopoverTrigger, PopoverContent, PopoverBody, useDisclosure } from '@chakra-ui/react'; import { PopoverTrigger, PopoverContent, PopoverBody, useDisclosure } from '@chakra-ui/react';
import _debounce from 'lodash/debounce'; import { debounce } from 'es-toolkit';
import type { FormEvent, FocusEvent } from 'react'; import type { FormEvent, FocusEvent } from 'react';
import React from 'react'; import React from 'react';
...@@ -59,7 +59,7 @@ const SearchResultsInput = ({ searchTerm, handleSubmit, handleSearchTermChange } ...@@ -59,7 +59,7 @@ const SearchResultsInput = ({ searchTerm, handleSubmit, handleSearchTermChange }
} }
calculateMenuWidth(); calculateMenuWidth();
const resizeHandler = _debounce(calculateMenuWidth, 200); const resizeHandler = debounce(calculateMenuWidth, 200);
const resizeObserver = new ResizeObserver(resizeHandler); const resizeObserver = new ResizeObserver(resizeHandler);
resizeObserver.observe(inputRef.current); resizeObserver.observe(inputRef.current);
......
import { Box, useColorModeValue } from '@chakra-ui/react'; import { Box, useColorModeValue } from '@chakra-ui/react';
import _debounce from 'lodash/debounce'; import { debounce } from 'es-toolkit';
import React, { useRef, useEffect, useState, useCallback } from 'react'; import React, { useRef, useEffect, useState, useCallback } from 'react';
const CUT_HEIGHT = 144; const CUT_HEIGHT = 144;
...@@ -25,7 +25,7 @@ const AccountPageDescription = ({ children, allowCut = true }: { children: React ...@@ -25,7 +25,7 @@ const AccountPageDescription = ({ children, allowCut = true }: { children: React
} }
calculateCut(); calculateCut();
const resizeHandler = _debounce(calculateCut, 300); const resizeHandler = debounce(calculateCut, 300);
window.addEventListener('resize', resizeHandler); window.addEventListener('resize', resizeHandler);
return function cleanup() { return function cleanup() {
window.removeEventListener('resize', resizeHandler); window.removeEventListener('resize', resizeHandler);
......
...@@ -25,7 +25,7 @@ interface Props { ...@@ -25,7 +25,7 @@ interface Props {
const ERROR_TEXTS: Record<string, { title: string; text: string }> = { const ERROR_TEXTS: Record<string, { title: string; text: string }> = {
'403': { '403': {
title: 'Forbidden', title: 'Alert',
text: 'Access to this resource is restricted.', text: 'Access to this resource is restricted.',
}, },
'404': { '404': {
......
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
import type { As } from '@chakra-ui/react'; import type { As } from '@chakra-ui/react';
import { Tooltip, chakra } from '@chakra-ui/react'; import { Tooltip, chakra } from '@chakra-ui/react';
import _debounce from 'lodash/debounce'; import { debounce } from 'es-toolkit';
import React, { useCallback, useEffect, useRef } from 'react'; import React, { useCallback, useEffect, useRef } from 'react';
import type { FontFace } from 'use-font-face-observer'; import type { FontFace } from 'use-font-face-observer';
import useFontFaceObserver from 'use-font-face-observer'; import useFontFaceObserver from 'use-font-face-observer';
...@@ -81,7 +81,7 @@ const HashStringShortenDynamic = ({ hash, fontWeight = '400', isTooltipDisabled, ...@@ -81,7 +81,7 @@ const HashStringShortenDynamic = ({ hash, fontWeight = '400', isTooltipDisabled,
}, [ calculateString, isFontFaceLoaded ]); }, [ calculateString, isFontFaceLoaded ]);
useEffect(() => { useEffect(() => {
const resizeHandler = _debounce(calculateString, 100); const resizeHandler = debounce(calculateString, 100);
const resizeObserver = new ResizeObserver(resizeHandler); const resizeObserver = new ResizeObserver(resizeHandler);
resizeObserver.observe(document.body); resizeObserver.observe(document.body);
......
...@@ -50,7 +50,7 @@ const NetworkAddToWallet = () => { ...@@ -50,7 +50,7 @@ const NetworkAddToWallet = () => {
} }
}, [ addOrSwitchChain, provider, toast, wallet ]); }, [ addOrSwitchChain, provider, toast, wallet ]);
if (!provider || !wallet || !config.chain.rpcUrl || !feature.isEnabled) { if (!provider || !wallet || !config.chain.rpcUrls.length || !feature.isEnabled) {
return null; return null;
} }
......
import { Heading, Flex, Tooltip, Link, chakra, useDisclosure } from '@chakra-ui/react'; import { Heading, Flex, Tooltip, Link, chakra, useDisclosure } from '@chakra-ui/react';
import _debounce from 'lodash/debounce'; import { debounce } from 'es-toolkit';
import React from 'react'; import React from 'react';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
...@@ -94,7 +94,7 @@ const PageTitle = ({ title, contentAfter, withTextAd, backLink, className, isLoa ...@@ -94,7 +94,7 @@ const PageTitle = ({ title, contentAfter, withTextAd, backLink, className, isLoa
}, [ isLoading, updatedTruncateState ]); }, [ isLoading, updatedTruncateState ]);
React.useEffect(() => { React.useEffect(() => {
const handleResize = _debounce(updatedTruncateState, 1000); const handleResize = debounce(updatedTruncateState, 1000);
window.addEventListener('resize', handleResize); window.addEventListener('resize', handleResize);
return function cleanup() { return function cleanup() {
......
import type { ChakraProps, ThemingProps } from '@chakra-ui/react'; import type { ChakraProps, ThemingProps } from '@chakra-ui/react';
import { chakra } from '@chakra-ui/react'; import { chakra } from '@chakra-ui/react';
import _pickBy from 'lodash/pickBy'; import { pickBy } from 'es-toolkit';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React, { useEffect, useRef } from 'react'; import React, { useEffect, useRef } from 'react';
...@@ -42,7 +42,7 @@ const RoutedTabs = ({ ...@@ -42,7 +42,7 @@ const RoutedTabs = ({
const handleTabChange = React.useCallback((index: number) => { const handleTabChange = React.useCallback((index: number) => {
const nextTab = tabs[index]; const nextTab = tabs[index];
const queryForPathname = _pickBy(router.query, (value, key) => router.pathname.includes(`[${ key }]`)); const queryForPathname = pickBy(router.query, (value, key) => router.pathname.includes(`[${ key }]`));
const tabId = Array.isArray(nextTab.id) ? nextTab.id[0] : nextTab.id; const tabId = Array.isArray(nextTab.id) ? nextTab.id[0] : nextTab.id;
router.push( router.push(
{ pathname: router.pathname, query: { ...queryForPathname, tab: tabId } }, { pathname: router.pathname, query: { ...queryForPathname, tab: tabId } },
......
...@@ -6,7 +6,7 @@ import { ...@@ -6,7 +6,7 @@ import {
TabPanels, TabPanels,
chakra, chakra,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import _debounce from 'lodash/debounce'; import { debounce } from 'es-toolkit';
import React, { useEffect, useRef, useState } from 'react'; import React, { useEffect, useRef, useState } from 'react';
import type { TabItem } from './types'; import type { TabItem } from './types';
...@@ -69,7 +69,7 @@ const TabsWithScroll = ({ ...@@ -69,7 +69,7 @@ const TabsWithScroll = ({
}, [ defaultTabIndex ]); }, [ defaultTabIndex ]);
React.useEffect(() => { React.useEffect(() => {
const resizeHandler = _debounce(() => { const resizeHandler = debounce(() => {
setScreenWidth(window.innerWidth); setScreenWidth(window.innerWidth);
}, 100); }, 100);
const resizeObserver = new ResizeObserver(resizeHandler); const resizeObserver = new ResizeObserver(resizeHandler);
......
import { Thead, useColorModeValue } from '@chakra-ui/react'; import { Thead, useColorModeValue } from '@chakra-ui/react';
import type { TableHeadProps, PositionProps } from '@chakra-ui/react'; import type { TableHeadProps, PositionProps } from '@chakra-ui/react';
import throttle from 'lodash/throttle'; import { throttle } from 'es-toolkit';
import React from 'react'; import React from 'react';
interface Props extends TableHeadProps { interface Props extends TableHeadProps {
......
import type { PlacementWithLogical } from '@chakra-ui/react'; import type { PlacementWithLogical } from '@chakra-ui/react';
import { Tooltip, useDisclosure } from '@chakra-ui/react'; import { Tooltip, useDisclosure } from '@chakra-ui/react';
import debounce from 'lodash/debounce'; import { debounce } from 'es-toolkit';
import React from 'react'; import React from 'react';
import useFontFaceObserver from 'use-font-face-observer'; import useFontFaceObserver from 'use-font-face-observer';
......
import { Box, Flex, chakra, useColorModeValue } from '@chakra-ui/react'; import { Box, Flex, chakra, useColorModeValue } from '@chakra-ui/react';
import clamp from 'lodash/clamp'; import { clamp } from 'es-toolkit';
import React from 'react'; import React from 'react';
import Skeleton from 'ui/shared/chakra/Skeleton'; import Skeleton from 'ui/shared/chakra/Skeleton';
......
...@@ -63,14 +63,14 @@ const ChartWidgetGraph = ({ ...@@ -63,14 +63,14 @@ const ChartWidgetGraph = ({
const axesConfig = React.useMemo(() => { const axesConfig = React.useMemo(() => {
return { return {
x: { x: {
ticks: isEnlarged ? 8 : 4, ticks: isEnlarged && !isMobile ? 8 : 4,
}, },
y: { y: {
ticks: isEnlarged ? 6 : 3, ticks: isEnlarged ? 6 : 3,
nice: true, nice: true,
}, },
}; };
}, [ isEnlarged ]); }, [ isEnlarged, isMobile ]);
const { const {
ref, ref,
......
import * as d3 from 'd3'; import * as d3 from 'd3';
import _clamp from 'lodash/clamp'; import { clamp } from 'es-toolkit';
import React from 'react'; import React from 'react';
import { POINT_SIZE } from './utils'; import { POINT_SIZE } from './utils';
...@@ -69,33 +69,33 @@ function calculatePosition({ pointX, pointY, canvasWidth, canvasHeight, nodeWidt ...@@ -69,33 +69,33 @@ function calculatePosition({ pointX, pointY, canvasWidth, canvasHeight, nodeWidt
// right // right
if (pointX + offset + nodeWidth <= canvasWidth) { if (pointX + offset + nodeWidth <= canvasWidth) {
const x = pointX + offset; const x = pointX + offset;
const y = _clamp(pointY - nodeHeight / 2, 0, canvasHeight - nodeHeight); const y = clamp(pointY - nodeHeight / 2, 0, canvasHeight - nodeHeight);
return [ x, y ]; return [ x, y ];
} }
// left // left
if (nodeWidth + offset <= pointX) { if (nodeWidth + offset <= pointX) {
const x = pointX - offset - nodeWidth; const x = pointX - offset - nodeWidth;
const y = _clamp(pointY - nodeHeight / 2, 0, canvasHeight - nodeHeight); const y = clamp(pointY - nodeHeight / 2, 0, canvasHeight - nodeHeight);
return [ x, y ]; return [ x, y ];
} }
// top // top
if (nodeHeight + offset <= pointY) { if (nodeHeight + offset <= pointY) {
const x = _clamp(pointX - nodeWidth / 2, 0, canvasWidth - nodeWidth); const x = clamp(pointX - nodeWidth / 2, 0, canvasWidth - nodeWidth);
const y = pointY - offset - nodeHeight; const y = pointY - offset - nodeHeight;
return [ x, y ]; return [ x, y ];
} }
// bottom // bottom
if (pointY + offset + nodeHeight <= canvasHeight) { if (pointY + offset + nodeHeight <= canvasHeight) {
const x = _clamp(pointX - nodeWidth / 2, 0, canvasWidth - nodeWidth); const x = clamp(pointX - nodeWidth / 2, 0, canvasWidth - nodeWidth);
const y = pointY + offset; const y = pointY + offset;
return [ x, y ]; return [ x, y ];
} }
const x = _clamp(pointX / 2, 0, canvasWidth - nodeWidth); const x = clamp(pointX / 2, 0, canvasWidth - nodeWidth);
const y = _clamp(pointY / 2, 0, canvasHeight - nodeHeight); const y = clamp(pointY / 2, 0, canvasHeight - nodeHeight);
return [ x, y ]; return [ x, y ];
} }
import _range from 'lodash/range'; import { range } from 'es-toolkit';
import React from 'react'; import React from 'react';
export default function useChartLegend(dataLength: number) { export default function useChartLegend(dataLength: number) {
const [ selectedLines, setSelectedLines ] = React.useState<Array<number>>(_range(dataLength)); const [ selectedLines, setSelectedLines ] = React.useState<Array<number>>(range(dataLength));
const handleLegendItemClick = React.useCallback((index: number) => { const handleLegendItemClick = React.useCallback((index: number) => {
const nextSelectedLines = selectedLines.includes(index) ? selectedLines.filter((item) => item !== index) : [ ...selectedLines, index ]; const nextSelectedLines = selectedLines.includes(index) ? selectedLines.filter((item) => item !== index) : [ ...selectedLines, index ];
......
import * as d3 from 'd3'; import * as d3 from 'd3';
import _maxBy from 'lodash/maxBy'; import { maxBy, uniq } from 'es-toolkit';
import _unique from 'lodash/uniq';
import type { AxesConfig, AxisConfig, TimeChartData } from '../types'; import type { AxesConfig, AxisConfig, TimeChartData } from '../types';
import { WEEK, MONTH, YEAR } from 'lib/consts'; import { MONTH, YEAR } from 'lib/consts';
export const DEFAULT_MAXIMUM_SIGNIFICANT_DIGITS = 2; export const DEFAULT_MAXIMUM_SIGNIFICANT_DIGITS = 2;
export const DEFAULT_MAXIMUM_FRACTION_DIGITS = 3; export const DEFAULT_MAXIMUM_FRACTION_DIGITS = 3;
...@@ -48,14 +47,12 @@ const tickFormatterX = (axis: d3.Axis<d3.NumberValue>) => (d: d3.AxisDomain) => ...@@ -48,14 +47,12 @@ const tickFormatterX = (axis: d3.Axis<d3.NumberValue>) => (d: d3.AxisDomain) =>
const span = Number(extent[1]) - Number(extent[0]); const span = Number(extent[1]) - Number(extent[0]);
if (span > YEAR) { if (span > 2 * YEAR) {
format = d3.timeFormat('%Y'); format = d3.timeFormat('%Y');
} else if (span > 2 * MONTH) { } else if (span > 4 * MONTH) {
format = d3.timeFormat('%b'); format = d3.timeFormat('%b \'%y');
} else if (span > WEEK) {
format = d3.timeFormat('%b %d');
} else { } else {
format = d3.timeFormat('%a %d'); format = d3.timeFormat('%d %b');
} }
return format(d as Date); return format(d as Date);
...@@ -90,8 +87,8 @@ function getYLabelFormatParams(ticks: Array<number>, maximumSignificantDigits = ...@@ -90,8 +87,8 @@ function getYLabelFormatParams(ticks: Array<number>, maximumSignificantDigits =
notation: 'compact' as const, notation: 'compact' as const,
}; };
const uniqTicksStr = _unique(ticks.map((tick) => tick.toLocaleString(undefined, params))); const uniqTicksStr = uniq(ticks.map((tick) => tick.toLocaleString(undefined, params)));
const maxLabelLength = _maxBy(uniqTicksStr, (items) => items.length)?.length ?? DEFAULT_LABEL_LENGTH; const maxLabelLength = maxBy(uniqTicksStr, (items) => items.length)?.length ?? DEFAULT_LABEL_LENGTH;
if (uniqTicksStr.length === ticks.length || maximumSignificantDigits === MAXIMUM_SIGNIFICANT_DIGITS_LIMIT) { if (uniqTicksStr.length === ticks.length || maximumSignificantDigits === MAXIMUM_SIGNIFICANT_DIGITS_LIMIT) {
return { ...params, maxLabelLength }; return { ...params, maxLabelLength };
......
...@@ -18,6 +18,10 @@ import AddressIdenticon from './AddressIdenticon'; ...@@ -18,6 +18,10 @@ import AddressIdenticon from './AddressIdenticon';
type LinkProps = EntityBase.LinkBaseProps & Pick<EntityProps, 'address'>; type LinkProps = EntityBase.LinkBaseProps & Pick<EntityProps, 'address'>;
const getDisplayedAddress = (address: AddressProp, altHash?: string) => {
return address.filecoin?.robust ?? address.filecoin?.id ?? altHash ?? address.hash;
};
const Link = chakra((props: LinkProps) => { const Link = chakra((props: LinkProps) => {
const defaultHref = route({ pathname: '/address/[hash]', query: { ...props.query, hash: props.address.hash } }); const defaultHref = route({ pathname: '/address/[hash]', query: { ...props.query, hash: props.address.hash } });
...@@ -80,7 +84,7 @@ const Icon = (props: IconProps) => { ...@@ -80,7 +84,7 @@ const Icon = (props: IconProps) => {
<Flex marginRight={ styles.marginRight }> <Flex marginRight={ styles.marginRight }>
<AddressIdenticon <AddressIdenticon
size={ props.size === 'lg' ? 30 : 20 } size={ props.size === 'lg' ? 30 : 20 }
hash={ props.address.filecoin?.robust ?? props.address.hash } hash={ getDisplayedAddress(props.address) }
/> />
</Flex> </Flex>
); );
...@@ -89,6 +93,7 @@ const Icon = (props: IconProps) => { ...@@ -89,6 +93,7 @@ const Icon = (props: IconProps) => {
export type ContentProps = Omit<EntityBase.ContentBaseProps, 'text'> & Pick<EntityProps, 'address'> & { altHash?: string }; export type ContentProps = Omit<EntityBase.ContentBaseProps, 'text'> & Pick<EntityProps, 'address'> & { altHash?: string };
const Content = chakra((props: ContentProps) => { const Content = chakra((props: ContentProps) => {
const displayedAddress = getDisplayedAddress(props.address, props.altHash);
const nameTag = props.address.metadata?.tags.find(tag => tag.tagType === 'name')?.name; const nameTag = props.address.metadata?.tags.find(tag => tag.tagType === 'name')?.name;
const nameText = nameTag || props.address.ens_domain_name || props.address.name; const nameText = nameTag || props.address.ens_domain_name || props.address.name;
...@@ -102,7 +107,9 @@ const Content = chakra((props: ContentProps) => { ...@@ -102,7 +107,9 @@ const Content = chakra((props: ContentProps) => {
const label = ( const label = (
<VStack gap={ 0 } py={ 1 } color="inherit"> <VStack gap={ 0 } py={ 1 } color="inherit">
<Box fontWeight={ 600 } whiteSpace="pre-wrap" wordBreak="break-word">{ nameText }</Box> <Box fontWeight={ 600 } whiteSpace="pre-wrap" wordBreak="break-word">{ nameText }</Box>
<Box whiteSpace="pre-wrap" wordBreak="break-word">{ props.address.filecoin?.robust ?? props.altHash ?? props.address.hash }</Box> <Box whiteSpace="pre-wrap" wordBreak="break-word">
{ displayedAddress }
</Box>
</VStack> </VStack>
); );
...@@ -118,7 +125,7 @@ const Content = chakra((props: ContentProps) => { ...@@ -118,7 +125,7 @@ const Content = chakra((props: ContentProps) => {
return ( return (
<EntityBase.Content <EntityBase.Content
{ ...props } { ...props }
text={ props.address.filecoin?.robust ?? props.altHash ?? props.address.hash } text={ displayedAddress }
/> />
); );
}); });
...@@ -129,7 +136,7 @@ const Copy = (props: CopyProps) => { ...@@ -129,7 +136,7 @@ const Copy = (props: CopyProps) => {
return ( return (
<EntityBase.Copy <EntityBase.Copy
{ ...props } { ...props }
text={ props.address.filecoin?.robust ?? props.altHash ?? props.address.hash } text={ getDisplayedAddress(props.address, props.altHash) }
/> />
); );
}; };
......
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';
...@@ -18,7 +18,7 @@ const defaultProps = { ...@@ -18,7 +18,7 @@ const defaultProps = {
isRequired: true, isRequired: true,
placeholder: 'Compiler', placeholder: 'Compiler',
name: 'compiler', name: 'compiler',
onChange: _noop, onChange: noop,
}; };
[ 'md' as const, 'lg' as const ].forEach((size) => { [ 'md' as const, 'lg' as const ].forEach((size) => {
......
import type { HTMLChakraProps } from '@chakra-ui/react'; import type { HTMLChakraProps } from '@chakra-ui/react';
import { Box, Tab, TabList, TabPanel, TabPanels, Tabs, useBoolean } from '@chakra-ui/react'; import { Box, Tab, TabList, TabPanel, TabPanels, Tabs, useBoolean } from '@chakra-ui/react';
import _throttle from 'lodash/throttle'; import { throttle } from 'es-toolkit';
import type * as monaco from 'monaco-editor/esm/vs/editor/editor.api'; import type * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
import React from 'react'; import React from 'react';
...@@ -47,7 +47,7 @@ const CodeEditorSideBar = ({ onFileSelect, data, monaco, editor, selectedFile, m ...@@ -47,7 +47,7 @@ const CodeEditorSideBar = ({ onFileSelect, data, monaco, editor, selectedFile, m
letterSpacing: 0.3, letterSpacing: 0.3,
}; };
const handleScrollThrottled = React.useRef(_throttle((event: React.SyntheticEvent) => { const handleScrollThrottled = React.useRef(throttle((event: React.SyntheticEvent) => {
setIsStuck((event.target as HTMLDivElement).scrollTop > 0); setIsStuck((event.target as HTMLDivElement).scrollTop > 0);
}, 100)); }, 100));
......
...@@ -367,7 +367,7 @@ describe('if there are multiple pages', () => { ...@@ -367,7 +367,7 @@ describe('if there are multiple pages', () => {
describe('if there is page query param in URL', () => { describe('if there is page query param in URL', () => {
it('sets this param as the page number', async() => { it('sets this param as the page number', async() => {
useRouter.mockReturnValueOnce({ ...router, query: { page: '3' } }); useRouter.mockReturnValue({ ...router, query: { page: '3' } });
const params: Params<'address_txs'> = { const params: Params<'address_txs'> = {
resourceName: 'address_txs', resourceName: 'address_txs',
...@@ -614,6 +614,56 @@ describe('queries with sorting', () => { ...@@ -614,6 +614,56 @@ describe('queries with sorting', () => {
}); });
}); });
describe('router query changes', () => {
it('refetches correct page when page number changes in URL', async() => {
const routerPush = jest.fn(() => Promise.resolve());
const router = {
pathname: '/current-route',
push: routerPush,
query: {
page: '3',
next_page_params: encodeURIComponent(JSON.stringify(responses.page_2.next_page_params)),
},
};
useRouter.mockReturnValue(router);
const params: Params<'address_txs'> = {
resourceName: 'address_txs',
pathParams: { hash: addressMock.hash },
};
fetch.once(JSON.stringify(responses.page_3), responseInit);
fetch.once(JSON.stringify(responses.page_2), responseInit);
const { result, rerender } = renderHook(() => useQueryWithPages(params), { wrapper });
await waitForApiResponse();
expect(result.current.data).toEqual(responses.page_3);
expect(result.current.pagination.page).toBe(3);
// Simulate URL change to page 2
useRouter.mockReturnValue({
...router,
query: {
page: '2',
next_page_params: encodeURIComponent(JSON.stringify(responses.page_1.next_page_params)),
},
});
rerender();
await waitForApiResponse();
expect(result.current.data).toEqual(responses.page_2);
expect(result.current.pagination).toMatchObject({
page: 2,
canGoBackwards: false,
hasNextPage: true,
isLoading: false,
isVisible: true,
});
});
});
async function waitForApiResponse() { async function waitForApiResponse() {
await flushPromises(); await flushPromises();
await act(flushPromises); await act(flushPromises);
......
import type { UseQueryResult } from '@tanstack/react-query'; import type { UseQueryResult } from '@tanstack/react-query';
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import omit from 'lodash/omit'; import { omit } from 'es-toolkit';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import { animateScroll } from 'react-scroll'; import { animateScroll } from 'react-scroll';
import type { PaginationParams } from './types'; import type { PaginationParams } from './types';
import type { Route } from 'nextjs-routes';
import type { PaginatedResources, PaginationFilters, PaginationSorting, ResourceError, ResourcePayload } from 'lib/api/resources'; import type { PaginatedResources, PaginationFilters, PaginationSorting, ResourceError, ResourcePayload } from 'lib/api/resources';
import { RESOURCES, SORTING_FIELDS } from 'lib/api/resources'; import { RESOURCES, SORTING_FIELDS } from 'lib/api/resources';
import type { Params as UseApiQueryParams } from 'lib/api/useApiQuery'; import type { Params as UseApiQueryParams } from 'lib/api/useApiQuery';
...@@ -26,6 +28,10 @@ type NextPageParams = Record<string, unknown>; ...@@ -26,6 +28,10 @@ type NextPageParams = Record<string, unknown>;
const INITIAL_PAGE_PARAMS = { '1': {} }; const INITIAL_PAGE_PARAMS = { '1': {} };
function getPageFromQuery(query: Route['query']) {
return query?.page && !Array.isArray(query.page) ? Number(query.page) : 1;
}
function getPaginationParamsFromQuery(queryString: string | Array<string> | undefined) { function getPaginationParamsFromQuery(queryString: string | Array<string> | undefined) {
if (queryString) { if (queryString) {
try { try {
...@@ -64,7 +70,7 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({ ...@@ -64,7 +70,7 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const router = useRouter(); const router = useRouter();
const [ page, setPage ] = React.useState<number>(router.query.page && !Array.isArray(router.query.page) ? Number(router.query.page) : 1); const [ page, setPage ] = React.useState<number>(getPageFromQuery(router.query));
const [ pageParams, setPageParams ] = React.useState<Record<number, NextPageParams>>({ const [ pageParams, setPageParams ] = React.useState<Record<number, NextPageParams>>({
[page]: getPaginationParamsFromQuery(router.query.next_page_params), [page]: getPaginationParamsFromQuery(router.query.next_page_params),
}); });
...@@ -148,7 +154,14 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({ ...@@ -148,7 +154,14 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({
}, [ queryClient, resourceName, router, scrollToTop ]); }, [ queryClient, resourceName, router, scrollToTop ]);
const onFilterChange = useCallback(<R extends PaginatedResources = Resource>(newFilters: PaginationFilters<R> | undefined) => { const onFilterChange = useCallback(<R extends PaginatedResources = Resource>(newFilters: PaginationFilters<R> | undefined) => {
const newQuery = omit<typeof router.query>(router.query, 'next_page_params', 'page', 'filterFields' in resource ? resource.filterFields : []); const newQuery: typeof router.query = omit(
router.query,
[
'next_page_params',
'page',
...('filterFields' in resource ? resource.filterFields : []),
],
);
if (newFilters) { if (newFilters) {
Object.entries(newFilters).forEach(([ key, value ]) => { Object.entries(newFilters).forEach(([ key, value ]) => {
const isValidValue = typeof value === 'boolean' || (value && value.length); const isValidValue = typeof value === 'boolean' || (value && value.length);
...@@ -173,8 +186,8 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({ ...@@ -173,8 +186,8 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({
}, [ router, resource, scrollToTop ]); }, [ router, resource, scrollToTop ]);
const onSortingChange = useCallback((newSorting: PaginationSorting<Resource> | undefined) => { const onSortingChange = useCallback((newSorting: PaginationSorting<Resource> | undefined) => {
const newQuery = { const newQuery: typeof router.query = {
...omit<typeof router.query>(router.query, 'next_page_params', 'page', SORTING_FIELDS), ...omit(router.query, [ 'next_page_params', 'page', ...SORTING_FIELDS ]),
...newSorting, ...newSorting,
}; };
scrollToTop(); scrollToTop();
...@@ -221,5 +234,17 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({ ...@@ -221,5 +234,17 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({
}, 0); }, 0);
}, []); }, []);
React.useEffect(() => {
const pageFromQuery = getPageFromQuery(router.query);
const nextPageParamsFromQuery = getPaginationParamsFromQuery(router.query.next_page_params);
setPage(pageFromQuery);
setPageParams(prev => ({
...prev,
[pageFromQuery]: nextPageParamsFromQuery,
}));
setHasPages(pageFromQuery > 1);
}, [ router.query ]);
return { ...queryResult, pagination, onFilterChange, onSortingChange }; return { ...queryResult, pagination, onFilterChange, onSortingChange };
} }
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';
...@@ -12,7 +12,7 @@ test('base view +@dark-mode', async({ render }) => { ...@@ -12,7 +12,7 @@ test('base view +@dark-mode', async({ render }) => {
<TagGroupSelect <TagGroupSelect
items={ [ { id: '1', title: 'Option 1' }, { id: '2', title: 'Option 2' }, { id: 'duck', title: 'Cute little duck' } ] } items={ [ { id: '1', title: 'Option 1' }, { id: '2', title: 'Option 2' }, { id: 'duck', title: 'Cute little duck' } ] }
value="duck" value="duck"
onChange={ _noop } onChange={ noop }
/>, />,
); );
......
import { Box, Select, VStack, Flex } from '@chakra-ui/react'; import { Box, Select, VStack, Flex } from '@chakra-ui/react';
import capitalize from 'lodash/capitalize'; import { capitalize } from 'es-toolkit';
import React from 'react'; import React from 'react';
import type { NetworkGroup, FeaturedNetwork } from 'types/networks'; import type { NetworkGroup, FeaturedNetwork } from 'types/networks';
......
...@@ -8,7 +8,7 @@ import { ...@@ -8,7 +8,7 @@ import {
useDisclosure, useDisclosure,
useOutsideClick, useOutsideClick,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import _debounce from 'lodash/debounce'; import { debounce } from 'es-toolkit';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import type { FormEvent } from 'react'; import type { FormEvent } from 'react';
import React from 'react'; import React from 'react';
...@@ -115,7 +115,7 @@ const SearchBar = ({ isHomepage }: Props) => { ...@@ -115,7 +115,7 @@ const SearchBar = ({ isHomepage }: Props) => {
} }
calculateMenuWidth(); calculateMenuWidth();
const resizeHandler = _debounce(calculateMenuWidth, 200); const resizeHandler = debounce(calculateMenuWidth, 200);
const resizeObserver = new ResizeObserver(resizeHandler); const resizeObserver = new ResizeObserver(resizeHandler);
resizeObserver.observe(inputRef.current); resizeObserver.observe(inputRef.current);
......
import { InputGroup, Input, InputLeftElement, chakra, useColorModeValue, forwardRef, InputRightElement, Center } from '@chakra-ui/react'; import { InputGroup, Input, InputLeftElement, chakra, useColorModeValue, forwardRef, InputRightElement, Center } from '@chakra-ui/react';
import throttle from 'lodash/throttle'; import { throttle } from 'es-toolkit';
import React from 'react'; import React from 'react';
import type { ChangeEvent, FormEvent, FocusEvent } from 'react'; import type { ChangeEvent, FormEvent, FocusEvent } from 'react';
......
import { Box, Tab, TabList, Tabs, Text, useColorModeValue } from '@chakra-ui/react'; import { Box, Tab, TabList, Tabs, Text, useColorModeValue } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query'; import type { UseQueryResult } from '@tanstack/react-query';
import throttle from 'lodash/throttle'; import { throttle } from 'es-toolkit';
import React from 'react'; import React from 'react';
import { scroller, Element } from 'react-scroll'; import { scroller, Element } from 'react-scroll';
......
...@@ -135,7 +135,7 @@ const TokenPageTitle = ({ tokenQuery, addressQuery, hash }: Props) => { ...@@ -135,7 +135,7 @@ const TokenPageTitle = ({ tokenQuery, addressQuery, hash }: Props) => {
<AccountActionsMenu isLoading={ isLoading }/> <AccountActionsMenu isLoading={ isLoading }/>
<Flex ml={{ base: 0, lg: 'auto' }} columnGap={ 2 } flexGrow={{ base: 1, lg: 0 }}> <Flex ml={{ base: 0, lg: 'auto' }} columnGap={ 2 } flexGrow={{ base: 1, lg: 0 }}>
<TokenVerifiedInfo verifiedInfoQuery={ verifiedInfoQuery }/> <TokenVerifiedInfo verifiedInfoQuery={ verifiedInfoQuery }/>
<NetworkExplorers type="token" pathParam={ addressHash } ml={{ base: 'auto', lg: 0 }}/> <NetworkExplorers type="token" pathParam={ addressHash.toLowerCase() } ml={{ base: 'auto', lg: 0 }}/>
</Flex> </Flex>
</Flex> </Flex>
); );
......
import _upperFirst from 'lodash/upperFirst'; import { upperFirst } from 'es-toolkit';
export function formatName(_name: string) { export function formatName(_name: string) {
const name = _name const name = _name
.replaceAll('_', ' ') .replaceAll('_', ' ')
.replaceAll(/\burl|nft|id\b/gi, (str) => str.toUpperCase()); .replaceAll(/\burl|nft|id\b/gi, (str) => str.toUpperCase());
return _upperFirst(name.trim()); return upperFirst(name.trim());
} }
const PINNED_FIELDS = [ 'name', 'description' ]; const PINNED_FIELDS = [ 'name', 'description' ];
......
import { Table, Tbody, Tr, Th, Box, Text, Show, Hide } from '@chakra-ui/react'; import { Table, Tbody, Tr, Th, Box, Text, Show, Hide } from '@chakra-ui/react';
import _chunk from 'lodash/chunk'; import { chunk } from 'es-toolkit';
import React, { useMemo, useState } from 'react'; import React, { useMemo, useState } from 'react';
import type { PaginationParams } from 'ui/shared/pagination/types'; import type { PaginationParams } from 'ui/shared/pagination/types';
...@@ -34,7 +34,7 @@ export default function TxAssetFlows(props: FlowViewProps) { ...@@ -34,7 +34,7 @@ export default function TxAssetFlows(props: FlowViewProps) {
const [ page, setPage ] = useState<number>(1); const [ page, setPage ] = useState<number>(1);
const ViewData = useMemo(() => (queryData ? generateFlowViewData(queryData) : []), [ queryData ]); const ViewData = useMemo(() => (queryData ? generateFlowViewData(queryData) : []), [ queryData ]);
const chunkedViewData = _chunk(ViewData, 50); const chunkedViewData = chunk(ViewData, 50);
const paginationProps: PaginationParams = useMemo(() => ({ const paginationProps: PaginationParams = useMemo(() => ({
onNextPageClick: () => setPage(page + 1), onNextPageClick: () => setPage(page + 1),
......
...@@ -94,7 +94,7 @@ const TxSubHeading = ({ hash, hasTag, txQuery }: Props) => { ...@@ -94,7 +94,7 @@ const TxSubHeading = ({ hash, hasTag, txQuery }: Props) => {
return ( return (
<TxInterpretation <TxInterpretation
summary={{ summary={{
summary_template: `{sender_hash} called {method} on {receiver_hash}`, summary_template: `{sender_hash} ${ txQuery.data.status === 'error' ? 'failed to call' : 'called' } {method} on {receiver_hash}`,
summary_template_variables: { summary_template_variables: {
sender_hash: { sender_hash: {
type: 'address', type: 'address',
......
import _findIndex from 'lodash/findIndex';
import type { NovesNft, NovesResponseData, NovesSentReceived, NovesToken } from 'types/api/noves'; import type { NovesNft, NovesResponseData, NovesSentReceived, NovesToken } from 'types/api/noves';
export interface NovesAction { export interface NovesAction {
...@@ -27,7 +25,7 @@ export function generateFlowViewData(data: NovesResponseData): Array<NovesFlowVi ...@@ -27,7 +25,7 @@ export function generateFlowViewData(data: NovesResponseData): Array<NovesFlowVi
const txItems = [ ...sent, ...received ]; const txItems = [ ...sent, ...received ];
const paidGasIndex = _findIndex(txItems, (item) => item.action === 'paidGas'); const paidGasIndex = txItems.findIndex((item) => item.action === 'paidGas');
if (paidGasIndex >= 0) { if (paidGasIndex >= 0) {
const element = txItems.splice(paidGasIndex, 1)[0]; const element = txItems.splice(paidGasIndex, 1)[0];
element.to.name = 'Validators'; element.to.name = 'Validators';
......
import _groupBy from 'lodash/groupBy'; import { groupBy, mapValues } from 'es-toolkit';
import _keysIn from 'lodash/keysIn';
import _mapValues from 'lodash/mapValues';
import type { NovesResponseData } from 'types/api/noves'; import type { NovesResponseData } from 'types/api/noves';
import type { TokenInfo } from 'types/api/token'; import type { TokenInfo } from 'types/api/token';
...@@ -49,28 +47,28 @@ export function getTokensData(data: NovesResponseData): TokensData { ...@@ -49,28 +47,28 @@ export function getTokensData(data: NovesResponseData): TokensData {
}); });
// Group tokens by property into arrays // Group tokens by property into arrays
const tokensGroupByname = _groupBy(tokens, 'name'); const tokensGroupByName = groupBy(tokens, (item) => item.name || 'null');
const tokensGroupBySymbol = _groupBy(tokens, 'symbol'); const tokensGroupBySymbol = groupBy(tokens, (item) => item.symbol || 'null');
const tokensGroupById = _groupBy(tokens, 'id'); const tokensGroupById = groupBy(tokens, (item) => item.id || 'null');
// Map properties to an object and remove duplicates // Map properties to an object and remove duplicates
const mappedNames = _mapValues(tokensGroupByname, (i) => { const mappedNames = mapValues(tokensGroupByName, (i) => {
return i[0]; return i[0];
}); });
const mappedSymbols = _mapValues(tokensGroupBySymbol, (i) => { const mappedSymbols = mapValues(tokensGroupBySymbol, (i) => {
return i[0]; return i[0];
}); });
const mappedIds = _mapValues(tokensGroupById, (i) => { const mappedIds = mapValues(tokensGroupById, (i) => {
return i[0]; return i[0];
}); });
const filters = [ 'undefined', 'null' ]; const filters = [ 'undefined', 'null' ];
// Array of keys to match in string // Array of keys to match in string
const nameList = _keysIn(mappedNames).filter(i => !filters.includes(i)); const nameList = Object.keys(mappedNames).filter(i => !filters.includes(i));
const symbolList = _keysIn(mappedSymbols).filter(i => !filters.includes(i)); const symbolList = Object.keys(mappedSymbols).filter(i => !filters.includes(i));
const idList = _keysIn(mappedIds).filter(i => !filters.includes(i)); const idList = Object.keys(mappedIds).filter(i => !filters.includes(i));
return { return {
nameList, nameList,
......
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import _chunk from 'lodash/chunk'; import { uniq, chunk } from 'es-toolkit';
import _uniq from 'lodash/uniq';
import React from 'react'; import React from 'react';
import type { NovesDescribeTxsResponse } from 'types/api/noves'; import type { NovesDescribeTxsResponse } from 'types/api/noves';
...@@ -16,8 +15,8 @@ const translateEnabled = feature.isEnabled && feature.provider === 'noves'; ...@@ -16,8 +15,8 @@ const translateEnabled = feature.isEnabled && feature.provider === 'noves';
export default function useDescribeTxs(items: Array<Transaction> | undefined, viewAsAccountAddress: string | undefined, isPlaceholderData: boolean) { export default function useDescribeTxs(items: Array<Transaction> | undefined, viewAsAccountAddress: string | undefined, isPlaceholderData: boolean) {
const apiFetch = useApiFetch(); const apiFetch = useApiFetch();
const txsHash = _uniq(items?.map(i => i.hash)); const txsHash = items ? uniq(items.map(i => i.hash)) : [];
const txChunks = _chunk(txsHash, 10); const txChunks = chunk(txsHash, 10);
const queryKey = { const queryKey = {
viewAsAccountAddress, viewAsAccountAddress,
......
...@@ -2705,9 +2705,9 @@ ...@@ -2705,9 +2705,9 @@
integrity sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ== integrity sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==
"@eslint/plugin-kit@^0.2.0": "@eslint/plugin-kit@^0.2.0":
version "0.2.2" version "0.2.4"
resolved "https://registry.yarnpkg.com/@eslint/plugin-kit/-/plugin-kit-0.2.2.tgz#5eff371953bc13e3f4d88150e2c53959f64f74f6" resolved "https://registry.yarnpkg.com/@eslint/plugin-kit/-/plugin-kit-0.2.4.tgz#2b78e7bb3755784bb13faa8932a1d994d6537792"
integrity sha512-CXtq5nR4Su+2I47WPOlWud98Y5Lv8Kyxp2ukhgFx/eW6Blm18VXJO5WuQylPugRo8nbluoi6GvvxBLqHcvqUUw== integrity "sha1-K3jnuzdVeEuxP6qJMqHZlNZTd5I= sha512-zSkKow6H5Kdm0ZUQUB2kV5JIXqoG0+uH5YADhaEHswm664N9Db8dXSi0nMJpacpMf+MyyglF1vnZohpEg5yUtg=="
dependencies: dependencies:
levn "^0.4.1" levn "^0.4.1"
...@@ -9180,7 +9180,7 @@ cross-fetch@^4.0.0: ...@@ -9180,7 +9180,7 @@ cross-fetch@^4.0.0:
dependencies: dependencies:
node-fetch "^2.6.12" node-fetch "^2.6.12"
cross-spawn@7.0.3, cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3: cross-spawn@7.0.3:
version "7.0.3" version "7.0.3"
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.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==
...@@ -9189,6 +9189,15 @@ cross-spawn@7.0.3, cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3: ...@@ -9189,6 +9189,15 @@ cross-spawn@7.0.3, cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3:
shebang-command "^2.0.0" shebang-command "^2.0.0"
which "^2.0.1" which "^2.0.1"
cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3:
version "7.0.6"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f"
integrity "sha1-ilj+ePANzXDDcEUXWd+/rwPo7p8= sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="
dependencies:
path-key "^3.1.0"
shebang-command "^2.0.0"
which "^2.0.1"
crypto-js@^4.2.0: crypto-js@^4.2.0:
version "4.2.0" version "4.2.0"
resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-4.2.0.tgz#4d931639ecdfd12ff80e8186dba6af2c2e856631" resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-4.2.0.tgz#4d931639ecdfd12ff80e8186dba6af2c2e856631"
...@@ -10039,9 +10048,9 @@ electron-to-chromium@^1.5.28: ...@@ -10039,9 +10048,9 @@ electron-to-chromium@^1.5.28:
integrity sha512-VufdJl+rzaKZoYVUijN13QcXVF5dWPZANeFTLNy+OSpHdDL5ynXTF35+60RSBbaQYB1ae723lQXHCrf4pyLsMw== integrity sha512-VufdJl+rzaKZoYVUijN13QcXVF5dWPZANeFTLNy+OSpHdDL5ynXTF35+60RSBbaQYB1ae723lQXHCrf4pyLsMw==
elliptic@^6.5.7: elliptic@^6.5.7:
version "6.5.7" version "6.6.1"
resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.7.tgz#8ec4da2cb2939926a1b9a73619d768207e647c8b" resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.6.1.tgz#3b8ffb02670bf69e382c7f65bf524c97c5405c06"
integrity sha512-ESVCtTwiA+XhY3wyh24QqRGBoP3rEdDUl3EDUUo9tft074fi19IrdpH7hLCMMP3CIj7jb3W96rn8lt/BqIlt5Q== integrity "sha1-O4/7AmcL9p44LH9lv1JMl8VAXAY= sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g=="
dependencies: dependencies:
bn.js "^4.11.9" bn.js "^4.11.9"
brorand "^1.1.0" brorand "^1.1.0"
...@@ -10367,6 +10376,11 @@ es-to-primitive@^1.2.1: ...@@ -10367,6 +10376,11 @@ es-to-primitive@^1.2.1:
is-date-object "^1.0.1" is-date-object "^1.0.1"
is-symbol "^1.0.2" is-symbol "^1.0.2"
es-toolkit@1.31.0:
version "1.31.0"
resolved "https://registry.yarnpkg.com/es-toolkit/-/es-toolkit-1.31.0.tgz#f4fc1382aea09cb239afa38f3c724a5658ff3163"
integrity sha512-vwS0lv/tzjM2/t4aZZRAgN9I9TP0MSkWuvt6By+hEXfG/uLs8yg2S1/ayRXH/x3pinbLgVJYT+eppueg3cM6tg==
esbuild@^0.21.3: esbuild@^0.21.3:
version "0.21.5" version "0.21.5"
resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.21.5.tgz#9ca301b120922959b766360d8ac830da0d02997d" resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.21.5.tgz#9ca301b120922959b766360d8ac830da0d02997d"
...@@ -13564,7 +13578,7 @@ lodash.throttle@^4.1.1: ...@@ -13564,7 +13578,7 @@ lodash.throttle@^4.1.1:
resolved "https://registry.yarnpkg.com/lodash.throttle/-/lodash.throttle-4.1.1.tgz#c23e91b710242ac70c37f1e1cda9274cc39bf2f4" resolved "https://registry.yarnpkg.com/lodash.throttle/-/lodash.throttle-4.1.1.tgz#c23e91b710242ac70c37f1e1cda9274cc39bf2f4"
integrity sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ== integrity sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==
lodash@^4.0.0, lodash@^4.15.0, lodash@^4.17.21: lodash@^4.15.0, lodash@^4.17.21:
version "4.17.21" version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
...@@ -13944,9 +13958,9 @@ nan@^2.14.0, nan@^2.14.1, nan@^2.17.0: ...@@ -13944,9 +13958,9 @@ nan@^2.14.0, nan@^2.14.1, nan@^2.17.0:
integrity sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ== integrity sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ==
nanoid@^3.3.6, nanoid@^3.3.7: nanoid@^3.3.6, 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 "sha1-sb4wML7jaq/xi6yzdeXM5SFoS68= sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w=="
napi-build-utils@^1.0.1: napi-build-utils@^1.0.1:
version "1.0.2" version "1.0.2"
......
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