Commit 6f19c4a1 authored by tom's avatar tom

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

parents e5829cf5 c9606a18
......@@ -17,6 +17,9 @@
/public/icons/sprite.svg
/public/icons/sprite.*.svg
/public/icons/README.md
/public/static/og_image.png
/public/sitemap.xml
/public/robots.txt
/analyze
# production
......
......@@ -33,6 +33,12 @@ WORKDIR /favicon-generator
COPY ./deploy/tools/favicon-generator/package.json ./deploy/tools/favicon-generator/yarn.lock ./
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 *******
......@@ -88,6 +94,12 @@ RUN cd ./deploy/tools/envs-validator && yarn build
# Copy dependencies and source code
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 ********
# *****************************
......@@ -122,11 +134,16 @@ COPY --chmod=755 ./deploy/scripts/validate_envs.sh .
COPY --chmod=755 ./deploy/scripts/make_envs_script.sh .
## Assets downloader
COPY --chmod=755 ./deploy/scripts/download_assets.sh .
## OG image generator
COPY ./deploy/scripts/og_image_generator.js .
## Favicon generator
COPY --chmod=755 ./deploy/scripts/favicon_generator.sh .
COPY --from=builder /app/deploy/tools/favicon-generator ./deploy/tools/favicon-generator
RUN ["chmod", "-R", "777", "./deploy/tools/favicon-generator"]
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 --from=builder /app/.env.registry .
......
import type { RollupType } from 'types/client/rollup';
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;
......@@ -17,6 +19,19 @@ const verificationType: NetworkVerificationType = (() => {
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({
id: getEnvValue('NEXT_PUBLIC_NETWORK_ID'),
name: getEnvValue('NEXT_PUBLIC_NETWORK_NAME'),
......@@ -32,7 +47,7 @@ const chain = Object.freeze({
},
hasMultipleGasCurrencies: getEnvValue('NEXT_PUBLIC_NETWORK_MULTIPLE_GAS_CURRENCIES') === 'true',
tokenStandard: getEnvValue('NEXT_PUBLIC_NETWORK_TOKEN_STANDARD_NAME') || 'ERC',
rpcUrl: getEnvValue('NEXT_PUBLIC_NETWORK_RPC_URL'),
rpcUrls,
isTestnet: getEnvValue('NEXT_PUBLIC_IS_TESTNET') === 'true',
verificationType,
});
......
......@@ -17,7 +17,7 @@ const config: Feature<{ walletConnect: { projectId: string } }> = (() => {
chain.currency.name &&
chain.currency.symbol &&
chain.currency.decimals &&
chain.rpcUrl &&
chain.rpcUrls.length > 0 &&
walletConnectProjectId
) {
return Object.freeze({
......
......@@ -33,7 +33,7 @@ const config: Feature<(
rating: { airtableApiKey: string; airtableBaseId: string } | undefined;
graphLinksUrl: string | undefined;
}> = (() => {
if (enabled === 'true' && chain.rpcUrl && submitFormUrl) {
if (enabled === 'true' && chain.rpcUrls.length > 0 && submitFormUrl) {
const props = {
submitFormUrl,
categoriesUrl,
......
......@@ -31,7 +31,7 @@ const config: Feature<{
type,
L1BaseUrl: stripTrailingSlash(L1BaseUrl),
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,
homepage: {
showLatestBlocks: getEnvValue('NEXT_PUBLIC_ROLLUP_HOMEPAGE_SHOW_LATEST_BLOCKS') === 'true',
......
import app from './app';
import { getEnvValue, getExternalAssetFilePath } from './utils';
const defaultImageUrl = '/static/og_placeholder.png';
const defaultImageUrl = '/static/og_image.png';
const meta = Object.freeze({
promoteBlockscoutInTitle: getEnvValue('NEXT_PUBLIC_PROMOTE_BLOCKSCOUT_IN_TITLE') === 'false' ? false : true,
......
......@@ -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_MOBILE={ "id": "632018", "width": "320", "height": "100" }
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_HOST=zkevm.blockscout.com
NEXT_PUBLIC_API_SPEC_URL=https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml
NEXT_PUBLIC_CONTRACT_CODE_IDES=[{'title':'Remix IDE','url':'https://remix.ethereum.org/?address={hash}&blockscout={domain}','icon_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/ide-icons/remix.png'}]
NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info.services.blockscout.com
NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/zkevm.json
NEXT_PUBLIC_GAME_BADGE_CLAIM_LINK=https://badges.blockscout.com/mint/sherblockHolmesBadge
NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0x25fcb396fc8652dcd0040f677a1dcc6fecff390ecafc815894379a3f254f1aa9
NEXT_PUBLIC_HAS_USER_OPS=true
NEXT_PUBLIC_HIDE_INDEXING_ALERT_INT_TXS=true
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_PLATE_TEXT_COLOR=rgba(255, 255, 255, 1)
NEXT_PUBLIC_MARKETPLACE_ENABLED=false
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_IS_ACCOUNT_SUPPORTED=true
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_METASUITES_ENABLED=true
NEXT_PUBLIC_NAVIGATION_HIGHLIGHTED_ROUTES=['/apps']
NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18
NEXT_PUBLIC_NETWORK_CURRENCY_NAME=MATIC
NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=MATIC
NEXT_PUBLIC_NETWORK_CURRENCY_NAME=ETH
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_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/polygon-short.svg
NEXT_PUBLIC_NETWORK_ID=1101
......
......@@ -35,6 +35,9 @@ export_envs_from_preset() {
# If there is a preset, load the environment variables from the its file
export_envs_from_preset
# Generate OG image
node --no-warnings ./og_image_generator.js
# Download external assets
./download_assets.sh ./public/assets/configs
......@@ -61,6 +64,9 @@ echo
# Create envs.js file with run-time environment variables for the client app
./make_envs_script.sh
# Generate sitemap.xml and robots.txt files
./sitemap_generator.sh
# Print list of enabled features
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:
integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
nanoid@^3.3.7:
version "3.3.7"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8"
integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==
version "3.3.8"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.8.tgz#b1be3030bee36aaff18bacb375e5cce521684baf"
integrity sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==
node-source-walk@^6.0.0, node-source-walk@^6.0.1, node-source-walk@^6.0.2:
version "6.0.2"
......
......@@ -587,7 +587,21 @@ const schema = yup
NEXT_PUBLIC_NETWORK_NAME: yup.string().required(),
NEXT_PUBLIC_NETWORK_SHORT_NAME: yup.string(),
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_WEI_NAME: yup.string(),
NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL: yup.string(),
......
......@@ -5,4 +5,5 @@ NEXT_PUBLIC_HOMEPAGE_STATS=[]
NEXT_PUBLIC_VIEWS_ADDRESS_FORMAT=['base16','bech32']
NEXT_PUBLIC_VIEWS_ADDRESS_BECH_32_PREFIX=foo
NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY=xxx
NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY=deprecated
\ No newline at end of file
NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY=deprecated
NEXT_PUBLIC_NETWORK_RPC_URL=['https://example.com','https://example2.com']
......@@ -340,9 +340,9 @@ commander@^2.20.0:
integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
cross-spawn@^7.0.3:
version "7.0.3"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==
version "7.0.6"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f"
integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==
dependencies:
path-key "^3.1.0"
shebang-command "^2.0.0"
......
......@@ -460,9 +460,9 @@ commander@^2.20.0:
integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
cross-spawn@^7.0.3:
version "7.0.3"
resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz"
integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==
version "7.0.6"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f"
integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==
dependencies:
path-key "^3.1.0"
shebang-command "^2.0.0"
......
......@@ -357,9 +357,9 @@ commander@^9.0.0:
integrity sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==
cross-spawn@^7.0.3:
version "7.0.3"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==
version "7.0.6"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f"
integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==
dependencies:
path-key "^3.1.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
| 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_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_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+ |
......@@ -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_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_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+ |
&nbsp;
......
......@@ -30,14 +30,9 @@ const RESTRICTED_MODULES = {
importNames: [ 'Popover', 'Menu', 'PinInput', 'useToast', 'Skeleton' ],
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: [
'icons/*',
'!lodash/*',
],
};
......@@ -428,6 +423,7 @@ export default tseslint.config(
'pages/**',
'nextjs/**',
'playwright/**',
'deploy/scripts/**',
'deploy/tools/**',
'middleware.ts',
'instrumentation*.ts',
......
<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>
import { getAddress } from 'viem';
import config from 'configs/app';
export default function getCheckedSummedAddress(address: string): string {
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) {
return address;
}
......
import { useQueryClient } from '@tanstack/react-query';
import _omit from 'lodash/omit';
import _pickBy from 'lodash/pickBy';
import { omit, pickBy } from 'es-toolkit';
import React from 'react';
import type { CsrfData } from 'types/client/account';
......@@ -38,7 +37,7 @@ export default function useApiFetch() {
const resource: ApiResource = RESOURCES[resourceName];
const url = buildUrl(resourceName, pathParams, queryParams);
const withBody = isBodyAllowed(fetchParams?.method);
const headers = _pickBy({
const headers = pickBy({
'x-endpoint': resource.endpoint && isNeedProxy() ? resource.endpoint : undefined,
Authorization: resource.endpoint && resource.needAuth ? apiToken : undefined,
'x-csrf-token': withBody && csrfToken ? csrfToken : undefined,
......@@ -55,7 +54,7 @@ export default function useApiFetch() {
// change condition here if something is changed
credentials: config.features.account.isEnabled ? 'include' : 'same-origin',
headers,
..._omit(fetchParams, 'headers'),
...(fetchParams ? omit(fetchParams, [ 'headers' ]) : {}),
},
{
resource: resource.path,
......
import clamp from 'lodash/clamp';
import throttle from 'lodash/throttle';
import { throttle, clamp } from 'es-toolkit';
import React from 'react';
const ScrollDirectionContext = React.createContext<'up' | 'down' | null>(null);
......
import _debounce from 'lodash/debounce';
import { debounce } from 'es-toolkit';
import type { LegacyRef } from 'react';
import React from 'react';
......@@ -19,7 +19,7 @@ export default function useClientRect<E extends Element>(): [ DOMRect | null, Le
return;
}
const resizeHandler = _debounce(() => {
const resizeHandler = debounce(() => {
setRect(nodeRef.current?.getBoundingClientRect() ?? null);
}, 100);
......
import throttle from 'lodash/throttle';
import { throttle } from 'es-toolkit';
import React from 'react';
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 { useInView } from 'react-intersection-observer';
......@@ -15,7 +15,7 @@ export default function useLazyRenderedList(list: Array<unknown>, isEnabled: boo
React.useEffect(() => {
if (inView) {
setRenderedItemsNum((prev) => _clamp(prev + STEP, 0, list.length));
setRenderedItemsNum((prev) => clamp(prev + STEP, 0, list.length));
}
}, [ inView, list.length ]);
......
......@@ -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.",
"opengraph": {
"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",
......
import _capitalize from 'lodash/capitalize';
import { capitalize } from 'es-toolkit';
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 mixpanel from 'mixpanel-browser';
import { useRouter } from 'next/router';
......@@ -40,12 +40,12 @@ export default function useMixpanelInit() {
'Viewport width': window.innerWidth,
'Viewport height': window.innerHeight,
Language: window.navigator.language,
'Device type': _capitalize(deviceType),
'Device type': capitalize(deviceType),
'User id': userId,
});
mixpanel.identify(userId);
userProfile.set({
'Device Type': _capitalize(deviceType),
'Device Type': capitalize(deviceType),
...(isAuth ? { 'With Account': true } : {}),
});
userProfile.setOnce({
......
import _compose from 'lodash/fp/compose';
import _mapValues from 'lodash/mapValues';
import { mapValues } from 'es-toolkit';
import type { NetworkExplorer } from 'types/networks';
......@@ -32,7 +31,7 @@ const networkExplorers: Array<NetworkExplorer> = (() => {
return config.UI.explorers.items.map((explorer) => ({
...explorer,
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';
......@@ -27,7 +27,7 @@ export function saveToRecentKeywords(value: string) {
}
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));
}
......
import _upperFirst from 'lodash/upperFirst';
import { upperFirst } from 'es-toolkit';
import type { Metadata, MetadataAttributes } from 'types/client/token';
......@@ -72,7 +72,7 @@ export default function attributesParser(attributes: Array<unknown>): Metadata['
return {
...formatValue(value, display, trait),
trait_type: _upperFirst(trait || 'property'),
trait_type: upperFirst(trait || 'property'),
};
})
.filter((item) => item?.value)
......
......@@ -12,7 +12,7 @@ const currentChain = {
},
rpcUrls: {
'default': {
http: [ config.chain.rpcUrl ?? '' ],
http: config.chain.rpcUrls,
},
},
blockExplorers: {
......
import _get from 'lodash/get';
import { get } from 'es-toolkit/compat';
import React from 'react';
import config from 'configs/app';
......@@ -25,7 +25,7 @@ export default function useAddOrSwitchChain() {
const errorObj = getErrorObj(error);
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.
if (code === 4902 || originalErrorCode === 4902) {
......@@ -37,7 +37,7 @@ export default function useAddOrSwitchChain() {
symbol: config.chain.currency.symbol,
decimals: config.chain.currency.decimals,
},
rpcUrls: [ config.chain.rpcUrl ],
rpcUrls: config.chain.rpcUrls,
blockExplorerUrls: [ config.app.baseUrl ],
} ] as never;
// in wagmi types for wallet_addEthereumChain method is not provided
......
import { WagmiAdapter } from '@reown/appkit-adapter-wagmi';
import { http } from 'viem';
import { fallback, http } from 'viem';
import { createConfig } from 'wagmi';
import config from 'configs/app';
......@@ -13,7 +13,11 @@ const wagmi = (() => {
const wagmiConfig = createConfig({
chains: [ currentChain ],
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,
batch: { multicall: { wait: 100 } },
......@@ -26,7 +30,7 @@ const wagmi = (() => {
networks: chains,
multiInjectedProviderDiscovery: true,
transports: {
[currentChain.id]: http(),
[currentChain.id]: fallback(config.chain.rpcUrls.map((url) => http(url))),
},
projectId: feature.walletConnect.projectId,
ssr: true,
......
import _padStart from 'lodash/padStart';
import { padStart } from 'es-toolkit/compat';
import type { BlockEpoch, BlockEpochElectionRewardDetails, BlockEpochElectionRewardDetailsResponse } from 'types/api/block';
......@@ -42,11 +42,11 @@ function getRewardDetailsItem(index: number): BlockEpochElectionRewardDetails {
amount: `${ 100 - index }210001063118670575`,
account: {
...addressMock.withoutName,
hash: `0x30D060F129817c4DE5fBc1366d53e19f43c8c6${ _padStart(String(index), 2, '0') }`,
hash: `0x30D060F129817c4DE5fBc1366d53e19f43c8c6${ padStart(String(index), 2, '0') }`,
},
associated_account: {
...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';
......@@ -51,17 +51,17 @@ export const withBtcLocked: HomeStats = {
export const withoutFiatPrices: HomeStats = {
...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 = {
...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 = {
...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 = {
......
......@@ -51,7 +51,7 @@ export function app(): CspDev.DirectiveDescriptor {
getFeaturePayload(config.features.rewards)?.api.endpoint,
// chain RPC server
config.chain.rpcUrl,
...config.chain.rpcUrls,
'https://infragrid.v.network', // RPC providers
// github (spec for api-docs page)
......
import type CspDev from 'csp-dev';
import { uniq } from 'es-toolkit';
export const KEY_WORDS = {
BLOB: 'blob:',
......@@ -11,17 +12,6 @@ export const KEY_WORDS = {
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>) {
return descriptors.reduce((result, item) => {
for (const _key in item) {
......@@ -50,7 +40,7 @@ export function makePolicyString(policyDescriptor: CspDev.DirectiveDescriptor) {
return;
}
const uniqueValues = unique(value);
const uniqueValues = uniq(value);
return [ key, uniqueValues.join(' ') ].join(' ');
})
.filter(Boolean)
......
import { pick } from 'es-toolkit';
import type { IncomingMessage } from 'http';
import _pick from 'lodash/pick';
import type { NextApiRequest } from 'next';
import type { NextApiRequestCookies } from 'next/dist/server/api-utils';
import type { RequestInit, Response } from 'node-fetch';
......@@ -21,7 +21,7 @@ export default function fetchFactory(
accept: _req.headers['accept'] || 'application/json',
'content-type': _req.headers['content-type'] || 'application/json',
cookie: apiToken ? `${ cookies.NAMES.API_TOKEN }=${ apiToken }` : '',
..._pick(_req.headers, [
...pick(_req.headers, [
'x-csrf-token',
'Authorization', // the old value, just in case
'authorization', // Node.js automatically lowercases headers
......
......@@ -33,6 +33,8 @@
"test:jest": "jest",
"test:jest:watch": "jest --watch",
"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: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 @@
"@opentelemetry/sdk-node": "0.49.1",
"@opentelemetry/sdk-trace-node": "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-adapter-wagmi": "1.6.0",
"@rollbar/react": "0.12.0-beta",
"@scure/base": "1.1.9",
"@slise/embed-react": "^2.2.0",
"@tanstack/react-query": "5.55.4",
"@tanstack/react-query-devtools": "5.55.4",
......@@ -78,6 +80,7 @@
"dappscout-iframe": "0.2.5",
"dayjs": "^1.11.5",
"dom-to-image": "^2.6.0",
"es-toolkit": "1.31.0",
"focus-visible": "^5.2.0",
"framer-motion": "^6.5.1",
"getit-sdk": "^1.0.4",
......@@ -86,7 +89,6 @@
"graphql": "^16.8.1",
"graphql-ws": "^5.11.3",
"js-cookie": "^3.0.1",
"lodash": "^4.0.0",
"magic-bytes.js": "1.8.0",
"mixpanel-browser": "^2.47.0",
"monaco-editor": "^0.34.1",
......
import _pick from 'lodash/pick';
import _pickBy from 'lodash/pickBy';
import { pick, pickBy } from 'es-toolkit';
import type { NextApiRequest, NextApiResponse } from 'next';
import fetchFactory from 'nextjs/utils/fetchProxy';
......@@ -18,7 +17,7 @@ const handler = async(nextReq: NextApiRequest, nextRes: NextApiResponse) => {
);
const apiRes = await fetchFactory(nextReq)(
url.toString(),
_pickBy(_pick(nextReq, [ 'body', 'method' ]), Boolean),
pickBy(pick(nextReq, [ 'body', 'method' ]), Boolean),
);
// proxy some headers from API
......
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 { getEnvValue } from 'configs/app/utils';
......@@ -43,7 +43,7 @@ const fixture: TestFixture<MockContractReadResponseFixture, { page: Page }> = as
value: params?.value,
};
if (_isEqual(params, callParams) && id) {
if (isEqual(params, callParams) && id) {
return route.fulfill({
status: 200,
json: {
......
import type { TestFixture, Page } from '@playwright/test';
import _isEqual from 'lodash/isEqual';
import { isEqual } from 'es-toolkit';
import type { PublicRpcSchema } from 'viem';
import { getEnvValue } from 'configs/app/utils';
......@@ -34,7 +34,7 @@ const fixture: TestFixture<MockRpcResponseFixture, { page: Page }> = async({ pag
...(rpcMock.Parameters ? { params: rpcMock.Parameters } : {}),
};
if (_isEqual(json, payload) && id !== undefined) {
if (isEqual(json, payload) && id !== undefined) {
return route.fulfill({
status: 200,
json: {
......
import './fonts.css';
import './index.css';
import { beforeMount } from '@playwright/experimental-ct-react/hooks';
import _defaultsDeep from 'lodash/defaultsDeep';
import MockDate from 'mockdate';
import * as router from 'next/router';
......@@ -12,12 +11,15 @@ const NEXT_ROUTER_MOCK = {
replace: () => Promise.resolve(),
};
beforeMount(async({ hooksConfig }) => {
beforeMount(async({ hooksConfig }: { hooksConfig?: { router: typeof router } }) => {
// Before mount, redefine useRouter to return mock value from test.
// @ts-ignore: I really want to redefine this property :)
// 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
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:
integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
cross-spawn@^7.0.3:
version "7.0.3"
resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz"
integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==
version "7.0.6"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f"
integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==
dependencies:
path-key "^3.1.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 = {
export type ZkEvmL2TxnBatchTxs = {
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;
};
......
import noop from 'lodash/noop';
import { noop } from 'es-toolkit';
import React from 'react';
import { test, expect } from 'playwright/lib';
......
import { Accordion, Box, Flex, Link } from '@chakra-ui/react';
import _range from 'lodash/range';
import { range } from 'es-toolkit';
import React from 'react';
import type { SmartContractMethod } from './types';
......@@ -39,7 +39,7 @@ const ContractAbi = ({ abi, addressHash, sourceAddress, tab, visibleItems }: Pro
}
if (expandedSections.length < abi.length) {
setExpandedSections(_range(0, abi.length));
setExpandedSections(range(0, abi.length));
} else {
setExpandedSections([]);
}
......
......@@ -28,7 +28,9 @@ interface 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 isOptional = isOptionalProp || isNativeCoin;
......@@ -46,6 +48,8 @@ const ContractMethodFieldInput = ({ data, hideLabel, path: name, className, isDi
const hasMultiplyButton = argTypeMatchInt && Number(argTypeMatchInt.power) >= 64;
React.useImperativeHandle(field.ref, () => ref.current);
const handleChange = React.useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
const formattedValue = format(event.target.value);
field.onChange(formattedValue); // data send back to hook form
......@@ -83,6 +87,42 @@ const ContractMethodFieldInput = ({ data, hideLabel, path: name, className, isDi
setValue(name, newValue, { shouldValidate: true });
}, [ 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;
return (
......@@ -107,9 +147,14 @@ const ContractMethodFieldInput = ({ data, hideLabel, path: name, className, isDi
thousandSeparator: ' ',
decimalScale: 0,
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 }
onPaste={ handlePaste }
required={ !isOptional }
isInvalid={ Boolean(error) }
placeholder={ data.type }
......@@ -148,7 +193,14 @@ const ContractMethodFieldInput = ({ data, hideLabel, path: name, className, isDi
Max
</Button>
)) }
{ hasMultiplyButton && <ContractMethodMultiplyButton onClick={ handleMultiplyButtonClick } isDisabled={ isDisabled }/> }
{ hasMultiplyButton && (
<ContractMethodMultiplyButton
onClick={ handleMultiplyButtonClick }
isDisabled={ isDisabled }
initialValue={ intPower }
onChange={ setIntPower }
/>
) }
</InputRightElement>
</InputGroup>
{ error && <Box color="error" fontSize="sm" lineHeight={ 5 } mt={ 1 }>{ error.message }</Box> }
......
......@@ -20,10 +20,12 @@ import IconSvg from 'ui/shared/IconSvg';
interface Props {
onClick: (power: number) => void;
isDisabled?: boolean;
initialValue: number;
onChange: (power: number) => void;
}
const ContractMethodMultiplyButton = ({ onClick, isDisabled }: Props) => {
const [ selectedOption, setSelectedOption ] = React.useState<number | undefined>(18);
const ContractMethodMultiplyButton = ({ onClick, isDisabled, initialValue, onChange }: Props) => {
const [ selectedOption, setSelectedOption ] = React.useState<number | undefined>(initialValue);
const [ customValue, setCustomValue ] = React.useState<number>();
const { isOpen, onToggle, onClose } = useDisclosure();
......@@ -35,13 +37,16 @@ const ContractMethodMultiplyButton = ({ onClick, isDisabled }: Props) => {
setSelectedOption((prev) => prev === id ? undefined : id);
setCustomValue(undefined);
onClose();
onChange(id);
}
}, [ onClose ]);
}, [ onClose, onChange ]);
const handleInputChange = React.useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
setCustomValue(Number(event.target.value));
const value = Number(event.target.value);
setCustomValue(value);
setSelectedOption(undefined);
}, []);
onChange(value);
}, [ onChange ]);
const value = selectedOption || customValue;
......
import _set from 'lodash/set';
import { set } from 'es-toolkit/compat';
import type { ContractAbiItemInput } from '../types';
......@@ -78,7 +78,7 @@ export function transformFormDataToMethodArgs(formData: ContractMethodFormFields
for (const field in formData) {
const value = formData[field];
_set(result, field.replaceAll(':', '.'), value);
set(result, field.replaceAll(':', '.'), value);
}
const filteredResult = filterOutEmptyItems(result);
......
import _pickBy from 'lodash/pickBy';
import { pickBy } from 'es-toolkit';
import { useRouter } from 'next/router';
import React from 'react';
......@@ -63,7 +63,7 @@ export default function useMethodsFilters({ abi }: Params) {
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(
{ pathname: router.pathname, query: { ...queryForPathname, tab: nextTab } },
undefined,
......
import { Image, Tooltip } from '@chakra-ui/react';
import _capitalize from 'lodash/capitalize';
import { capitalize } from 'es-toolkit';
import React from 'react';
import type { MultichainProviderConfigParsed } from 'types/client/multichainProviderConfig';
......@@ -25,10 +25,10 @@ const AddressMultichainButton = ({ item, addressHash, onClick, hasSingleProvider
const buttonContent = hasSingleProvider ? (
<>
{ buttonIcon }
{ _capitalize(item.name) }
{ capitalize(item.name) }
</>
) : (
<Tooltip label={ _capitalize(item.name) }>{ buttonIcon }</Tooltip>
<Tooltip label={ capitalize(item.name) }>{ buttonIcon }</Tooltip>
);
const linkProps = {
......
......@@ -12,7 +12,7 @@ import {
chakra,
} from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query';
import _clamp from 'lodash/clamp';
import { clamp } from 'es-toolkit';
import React from 'react';
import type * as bens from '@blockscout/bens-types';
......@@ -37,7 +37,7 @@ interface Props {
const DomainsGrid = ({ data }: { data: Array<bens.Domain> }) => {
return (
<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 }
rowGap={ 4 }
mt={ 2 }
......
import { Box, Flex, IconButton, Tooltip } from '@chakra-ui/react';
import { useQueryClient, useIsFetching } from '@tanstack/react-query';
import _sumBy from 'lodash/sumBy';
import { sumBy } from 'es-toolkit';
import NextLink from 'next/link';
import { useRouter } from 'next/router';
import React from 'react';
......@@ -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) {
return <Box py="6px">0</Box>;
}
......
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 React from 'react';
......@@ -26,7 +26,7 @@ interface Props {
const TokenSelectMenu = ({ erc20sort, erc1155sort, erc404sort, filteredData, onInputChange, onSortClick, searchTerm }: Props) => {
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 (
<>
......
import _mapValues from 'lodash/mapValues';
import { mapValues } from 'es-toolkit';
import type { ChangeEvent } from 'react';
import React from 'react';
......@@ -31,7 +31,7 @@ export default function useTokenSelect(data: FormattedData) {
}, []);
const filteredData = React.useMemo(() => {
return _mapValues(data, ({ items, isOverflow }) => ({
return mapValues(data, ({ items, isOverflow }) => ({
isOverflow,
items: items.filter(filterTokens(searchTerm.toLowerCase())),
}));
......
import BigNumber from 'bignumber.js';
import fpAdd from 'lodash/fp/add';
import type { AddressTokenBalance } from 'types/api/address';
import type { TokenType } from 'types/api/token';
......@@ -100,7 +99,7 @@ export const getTokensTotalInfo = (data: TokenSelectData) => {
const num = Object.values(data)
.map(({ items }) => items.length)
.reduce(fpAdd, 0);
.reduce((result, item) => result + item, 0);
const isOverflow = Object.values(data).some(({ isOverflow }) => isOverflow);
......
......@@ -118,7 +118,9 @@ export default function useAddressQuery({ hash, isEnabled = true }: Params): Add
}, [ rpcQuery.data, rpcQuery.isPlaceholderData ]);
const isRpcQuery = Boolean(
(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) &&
apiQuery.errorUpdateCount > 0 &&
rpcQuery.data &&
......
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 React from '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 React from '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 React from '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 type { AdvancedFilterParams } from 'types/api/advancedFilter';
......
import { Flex, Checkbox, CheckboxGroup, Spinner, chakra } from '@chakra-ui/react';
import differenceBy from 'lodash/differenceBy';
import isEqual from 'lodash/isEqual';
import { isEqual, differenceBy } from 'es-toolkit';
import type { ChangeEvent } from 'react';
import React from 'react';
......
import { Flex, Checkbox, CheckboxGroup } from '@chakra-ui/react';
import isEqual from 'lodash/isEqual';
import without from 'lodash/without';
import { isEqual, without } from 'es-toolkit';
import type { ChangeEvent } 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 { Grid, GridItem, Text, Link, Box, Tooltip } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import capitalize from 'lodash/capitalize';
import { capitalize } from 'es-toolkit';
import { useRouter } from 'next/router';
import React from 'react';
import { scroller, Element } from 'react-scroll';
......
import _padStart from 'lodash/padStart';
import { padStart } from 'es-toolkit/compat';
export default function splitSecondsInPeriods(value: number) {
const seconds = value % 60;
......@@ -7,9 +7,9 @@ export default function splitSecondsInPeriods(value: number) {
const days = (value - seconds - minutes * 60 - hours * 60 * 60) / (60 * 60 * 24);
return {
seconds: _padStart(String(seconds), 2, '0'),
minutes: _padStart(String(minutes), 2, '0'),
hours: _padStart(String(hours), 2, '0'),
days: _padStart(String(days), 2, '0'),
seconds: padStart(String(seconds), 2, '0'),
minutes: padStart(String(minutes), 2, '0'),
hours: padStart(String(hours), 2, '0'),
days: padStart(String(days), 2, '0'),
};
}
import { Flex, Text, Box, Tooltip } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import capitalize from 'lodash/capitalize';
import { capitalize } from 'es-toolkit';
import React from 'react';
import type { Block } from 'types/api/block';
......
import { Table, Tbody, Tr, Th } from '@chakra-ui/react';
import { capitalize } from 'es-toolkit';
import { AnimatePresence } from 'framer-motion';
import capitalize from 'lodash/capitalize';
import React from 'react';
import type { Block } from 'types/api/block';
......
import _get from 'lodash/get';
import { get } from 'es-toolkit/compat';
import React from 'react';
import { useFormContext } from 'react-hook-form';
......@@ -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`);
repoErrorRef.current = undefined;
trigger('repository_url');
onCommitHashChange(_get(response, '[0].sha'));
onCommitHashChange(get(response, '[0].sha'));
return;
} catch (error) {
repoErrorRef.current = 'GitHub repository not found';
......
......@@ -15,7 +15,7 @@ const ContractVerificationSolidityFoundry = () => {
const address = watch('address');
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-url '${ config.api.endpoint }/api/' \\
${ address || '<address>' } \\
......
......@@ -22,7 +22,7 @@ const ContractVerificationSolidityHardhat = ({ config: formConfig }: { config: S
solidity: "${ latestSolidityVersion || '0.8.24' }", // replace if necessary
networks: {
'${ chainNameSlug }': {
url: '${ config.chain.rpcUrl || `${ config.api.endpoint }/api/eth-rpc` }'
url: '${ config.chain.rpcUrls[0] || `${ config.api.endpoint }/api/eth-rpc` }'
},
},
etherscan: {
......
import _capitalize from 'lodash/capitalize';
import { capitalize } from 'es-toolkit';
import React from 'react';
import type { UseFormReturn } from 'react-hook-form';
......@@ -40,7 +40,7 @@ const CsvExportFormField = ({ formApi, name }: Props) => {
name={ name }
type="date"
max={ dayjs().format('YYYY-MM-DD') }
placeholder={ _capitalize(name) }
placeholder={ capitalize(name) }
isRequired
rules={{ validate }}
size={{ base: 'md', lg: 'lg' }}
......
......@@ -51,7 +51,7 @@ test('degradation view', async({ render, page, mockRpcResponse, mockApiResponse
});
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({
mask: [ page.locator(pwConfig.adsBannerSelector) ],
......
......@@ -69,7 +69,6 @@ const AddressPageContent = () => {
const tabsScrollRef = React.useRef<HTMLDivElement>(null);
const hash = getQueryParamString(router.query.hash);
const checkSummedHash = React.useMemo(() => getCheckedSummedAddress(hash), [ hash ]);
const checkDomainName = useCheckDomainNameParam(hash);
const checkAddressFormat = useCheckAddressFormat(hash);
......@@ -364,6 +363,10 @@ const AddressPageContent = () => {
return;
}, [ 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 = (
<Flex alignItems="center" w="100%" columnGap={ 2 } rowGap={ 2 } flexWrap={{ base: 'wrap', lg: 'nowrap' }}>
{ addressQuery.data?.ens_domain_name && (
......
......@@ -15,7 +15,7 @@ import {
HStack,
Link,
} from '@chakra-ui/react';
import omit from 'lodash/omit';
import { omit } from 'es-toolkit';
import { useRouter } from 'next/router';
import React from 'react';
......
......@@ -28,7 +28,7 @@ test('degradation view, details tab', async({ render, mockApiResponse, mockRpcRe
});
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();
});
......@@ -49,7 +49,7 @@ test('degradation view, txs tab', async({ render, mockApiResponse, mockRpcRespon
});
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();
});
......@@ -71,7 +71,7 @@ test('degradation view, withdrawals tab', async({ render, mockApiResponse, mockR
});
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();
});
import { chakra } from '@chakra-ui/react';
import capitalize from 'lodash/capitalize';
import { capitalize } from 'es-toolkit';
import { useRouter } from 'next/router';
import React from 'react';
......
......@@ -65,7 +65,7 @@ const MarketplaceAppContent = ({ address, data, isPending, appUrl }: Props) => {
blockscoutNetworkName: config.chain.name,
blockscoutNetworkId: Number(config.chain.id),
blockscoutNetworkCurrency: config.chain.currency,
blockscoutNetworkRpc: config.chain.rpcUrl,
blockscoutNetworkRpc: config.chain.rpcUrls[0],
};
iframeRef?.current?.contentWindow?.postMessage(message, data.url);
......@@ -159,7 +159,7 @@ const MarketplaceApp = () => {
<DappscoutIframeProvider
address={ address }
appUrl={ appUrl }
rpcUrl={ config.chain.rpcUrl }
rpcUrl={ config.chain.rpcUrls[0] }
sendTransaction={ sendTransaction }
signMessage={ signMessage }
signTypedData={ signTypedData }
......
......@@ -7,24 +7,40 @@ import { test, expect } from 'playwright/lib';
import Tokens from './Tokens';
test.beforeEach(async({ mockTextAd }) => {
test.beforeEach(async({ mockTextAd, mockAssetResponse }) => {
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 }) => {
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',
},
};
await mockApiResponse('tokens', allTokens);
const component = await render(
<div>
<Box h={{ base: '134px', lg: 6 }}/>
<Tokens/>
</div>,
);
await expect(component).toHaveScreenshot();
});
test('with search +@mobile +@dark-mode', async({ render, mockApiResponse }) => {
const filteredTokens = {
items: [
tokens.tokenInfoERC20a, tokens.tokenInfoERC20b, tokens.tokenInfoERC20c,
......@@ -42,10 +58,9 @@ test('base view +@mobile +@dark-mode', async({ render, mockApiResponse }) => {
</div>,
);
await expect(component).toHaveScreenshot();
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' }).blur();
await expect(component).toHaveScreenshot();
});
......
import { Flex } from '@chakra-ui/react';
import capitalize from 'lodash/capitalize';
import { capitalize } from 'es-toolkit';
import { useRouter } from 'next/router';
import React from 'react';
......
import _inRange from 'lodash/inRange';
import { inRange } from 'es-toolkit';
import { useRouter } from 'next/router';
import React from 'react';
......@@ -43,7 +43,7 @@ const UserOp = () => {
if (!userOpQuery.data) {
return true;
} else {
if (_inRange(
if (inRange(
Number(tt.log_index),
userOpQuery.data?.user_logs_start_index,
userOpQuery.data?.user_logs_start_index + userOpQuery.data?.user_logs_count,
......@@ -58,7 +58,7 @@ const UserOp = () => {
if (!userOpQuery.data) {
return true;
} 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 false;
......
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 type { FormSubmitResult } from './types';
......@@ -26,7 +26,7 @@ const PublicTagsSubmitResult = ({ data }: Props) => {
const hasErrors = groupedData.items.some((item) => item.error !== null);
const companyWebsite = makePrettyLink(groupedData.companyWebsite);
const startOverButtonQuery = hasErrors ? _pickBy({
const startOverButtonQuery = hasErrors ? pickBy({
requesterName: groupedData.requesterName,
requesterEmail: groupedData.requesterEmail,
companyName: groupedData.companyName,
......
import { chakra, Flex } from '@chakra-ui/react';
import type { GroupBase, SelectComponentsConfig, SingleValueProps } 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 { useFormContext } from 'react-hook-form';
......@@ -22,7 +22,7 @@ const PublicTagsSubmitFieldTagType = ({ index, tagTypes }: Props) => {
const typeOptions = React.useMemo(() => tagTypes?.map((type) => ({
value: type.type,
label: _capitalize(type.type),
label: capitalize(type.type),
})) ?? [], [ tagTypes ]);
const fieldValue = watch(`tags.${ index }.type`).value;
......
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 type { FormSubmitResultGrouped } from '../types';
......@@ -23,7 +23,7 @@ const PublicTagsSubmitResultWithErrors = ({ data }: Props) => {
<Flex flexDir="column" rowGap={ 3 }>
{ data.items.map((item, index) => {
const startOverButtonQuery = _pickBy({
const startOverButtonQuery = pickBy({
addresses: item.addresses,
requesterName: data.requesterName,
requesterEmail: data.requesterEmail,
......
import _isEqual from 'lodash/isEqual';
import _pickBy from 'lodash/pickBy';
import { pickBy, isEqual } from 'es-toolkit';
import type { FormFieldTag, FormFields, FormSubmitResult, FormSubmitResultGrouped, FormSubmitResultItemGrouped, SubmitRequestBody } from './types';
import type { UserInfo } from 'types/api/account';
......@@ -22,7 +21,7 @@ export function convertFormDataToRequestsBody(data: FormFields): Array<SubmitReq
name: tag.name,
tagType: tag.type.value,
description: data.description,
meta: _pickBy({
meta: pickBy({
bgColor: tag.bgColor,
textColor: tag.textColor,
tagUrl: tag.url,
......@@ -72,7 +71,7 @@ export function groupSubmitResult(data: FormSubmitResult | undefined): FormSubmi
// merge items with the same error and tags
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) {
existingItem.addresses.push(...item.addresses);
continue;
......
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 React from 'react';
......@@ -59,7 +59,7 @@ const SearchResultsInput = ({ searchTerm, handleSubmit, handleSearchTermChange }
}
calculateMenuWidth();
const resizeHandler = _debounce(calculateMenuWidth, 200);
const resizeHandler = debounce(calculateMenuWidth, 200);
const resizeObserver = new ResizeObserver(resizeHandler);
resizeObserver.observe(inputRef.current);
......
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';
const CUT_HEIGHT = 144;
......@@ -25,7 +25,7 @@ const AccountPageDescription = ({ children, allowCut = true }: { children: React
}
calculateCut();
const resizeHandler = _debounce(calculateCut, 300);
const resizeHandler = debounce(calculateCut, 300);
window.addEventListener('resize', resizeHandler);
return function cleanup() {
window.removeEventListener('resize', resizeHandler);
......
......@@ -25,7 +25,7 @@ interface Props {
const ERROR_TEXTS: Record<string, { title: string; text: string }> = {
'403': {
title: 'Forbidden',
title: 'Alert',
text: 'Access to this resource is restricted.',
},
'404': {
......
......@@ -10,7 +10,7 @@
import type { As } 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 type { FontFace } from 'use-font-face-observer';
import useFontFaceObserver from 'use-font-face-observer';
......@@ -81,7 +81,7 @@ const HashStringShortenDynamic = ({ hash, fontWeight = '400', isTooltipDisabled,
}, [ calculateString, isFontFaceLoaded ]);
useEffect(() => {
const resizeHandler = _debounce(calculateString, 100);
const resizeHandler = debounce(calculateString, 100);
const resizeObserver = new ResizeObserver(resizeHandler);
resizeObserver.observe(document.body);
......
......@@ -50,7 +50,7 @@ const NetworkAddToWallet = () => {
}
}, [ addOrSwitchChain, provider, toast, wallet ]);
if (!provider || !wallet || !config.chain.rpcUrl || !feature.isEnabled) {
if (!provider || !wallet || !config.chain.rpcUrls.length || !feature.isEnabled) {
return null;
}
......
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 useIsMobile from 'lib/hooks/useIsMobile';
......@@ -94,7 +94,7 @@ const PageTitle = ({ title, contentAfter, withTextAd, backLink, className, isLoa
}, [ isLoading, updatedTruncateState ]);
React.useEffect(() => {
const handleResize = _debounce(updatedTruncateState, 1000);
const handleResize = debounce(updatedTruncateState, 1000);
window.addEventListener('resize', handleResize);
return function cleanup() {
......
import type { ChakraProps, ThemingProps } 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 React, { useEffect, useRef } from 'react';
......@@ -42,7 +42,7 @@ const RoutedTabs = ({
const handleTabChange = React.useCallback((index: number) => {
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;
router.push(
{ pathname: router.pathname, query: { ...queryForPathname, tab: tabId } },
......
......@@ -6,7 +6,7 @@ import {
TabPanels,
chakra,
} from '@chakra-ui/react';
import _debounce from 'lodash/debounce';
import { debounce } from 'es-toolkit';
import React, { useEffect, useRef, useState } from 'react';
import type { TabItem } from './types';
......@@ -69,7 +69,7 @@ const TabsWithScroll = ({
}, [ defaultTabIndex ]);
React.useEffect(() => {
const resizeHandler = _debounce(() => {
const resizeHandler = debounce(() => {
setScreenWidth(window.innerWidth);
}, 100);
const resizeObserver = new ResizeObserver(resizeHandler);
......
import { Thead, useColorModeValue } 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';
interface Props extends TableHeadProps {
......
import type { PlacementWithLogical } 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 useFontFaceObserver from 'use-font-face-observer';
......
import { Box, Flex, chakra, useColorModeValue } from '@chakra-ui/react';
import clamp from 'lodash/clamp';
import { clamp } from 'es-toolkit';
import React from 'react';
import Skeleton from 'ui/shared/chakra/Skeleton';
......
......@@ -63,14 +63,14 @@ const ChartWidgetGraph = ({
const axesConfig = React.useMemo(() => {
return {
x: {
ticks: isEnlarged ? 8 : 4,
ticks: isEnlarged && !isMobile ? 8 : 4,
},
y: {
ticks: isEnlarged ? 6 : 3,
nice: true,
},
};
}, [ isEnlarged ]);
}, [ isEnlarged, isMobile ]);
const {
ref,
......
import * as d3 from 'd3';
import _clamp from 'lodash/clamp';
import { clamp } from 'es-toolkit';
import React from 'react';
import { POINT_SIZE } from './utils';
......@@ -69,33 +69,33 @@ function calculatePosition({ pointX, pointY, canvasWidth, canvasHeight, nodeWidt
// right
if (pointX + offset + nodeWidth <= canvasWidth) {
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 ];
}
// left
if (nodeWidth + offset <= pointX) {
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 ];
}
// top
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;
return [ x, y ];
}
// bottom
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;
return [ x, y ];
}
const x = _clamp(pointX / 2, 0, canvasWidth - nodeWidth);
const y = _clamp(pointY / 2, 0, canvasHeight - nodeHeight);
const x = clamp(pointX / 2, 0, canvasWidth - nodeWidth);
const y = clamp(pointY / 2, 0, canvasHeight - nodeHeight);
return [ x, y ];
}
import _range from 'lodash/range';
import { range } from 'es-toolkit';
import React from 'react';
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 nextSelectedLines = selectedLines.includes(index) ? selectedLines.filter((item) => item !== index) : [ ...selectedLines, index ];
......
import * as d3 from 'd3';
import _maxBy from 'lodash/maxBy';
import _unique from 'lodash/uniq';
import { maxBy, uniq } from 'es-toolkit';
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_FRACTION_DIGITS = 3;
......@@ -48,14 +47,12 @@ const tickFormatterX = (axis: d3.Axis<d3.NumberValue>) => (d: d3.AxisDomain) =>
const span = Number(extent[1]) - Number(extent[0]);
if (span > YEAR) {
if (span > 2 * YEAR) {
format = d3.timeFormat('%Y');
} else if (span > 2 * MONTH) {
format = d3.timeFormat('%b');
} else if (span > WEEK) {
format = d3.timeFormat('%b %d');
} else if (span > 4 * MONTH) {
format = d3.timeFormat('%b \'%y');
} else {
format = d3.timeFormat('%a %d');
format = d3.timeFormat('%d %b');
}
return format(d as Date);
......@@ -90,8 +87,8 @@ function getYLabelFormatParams(ticks: Array<number>, maximumSignificantDigits =
notation: 'compact' as const,
};
const uniqTicksStr = _unique(ticks.map((tick) => tick.toLocaleString(undefined, params)));
const maxLabelLength = _maxBy(uniqTicksStr, (items) => items.length)?.length ?? DEFAULT_LABEL_LENGTH;
const uniqTicksStr = uniq(ticks.map((tick) => tick.toLocaleString(undefined, params)));
const maxLabelLength = maxBy(uniqTicksStr, (items) => items.length)?.length ?? DEFAULT_LABEL_LENGTH;
if (uniqTicksStr.length === ticks.length || maximumSignificantDigits === MAXIMUM_SIGNIFICANT_DIGITS_LIMIT) {
return { ...params, maxLabelLength };
......
......@@ -18,6 +18,10 @@ import AddressIdenticon from './AddressIdenticon';
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 defaultHref = route({ pathname: '/address/[hash]', query: { ...props.query, hash: props.address.hash } });
......@@ -80,7 +84,7 @@ const Icon = (props: IconProps) => {
<Flex marginRight={ styles.marginRight }>
<AddressIdenticon
size={ props.size === 'lg' ? 30 : 20 }
hash={ props.address.filecoin?.robust ?? props.address.hash }
hash={ getDisplayedAddress(props.address) }
/>
</Flex>
);
......@@ -89,6 +93,7 @@ const Icon = (props: IconProps) => {
export type ContentProps = Omit<EntityBase.ContentBaseProps, 'text'> & Pick<EntityProps, 'address'> & { altHash?: string };
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 nameText = nameTag || props.address.ens_domain_name || props.address.name;
......@@ -102,7 +107,9 @@ const Content = chakra((props: ContentProps) => {
const label = (
<VStack gap={ 0 } py={ 1 } color="inherit">
<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>
);
......@@ -118,7 +125,7 @@ const Content = chakra((props: ContentProps) => {
return (
<EntityBase.Content
{ ...props }
text={ props.address.filecoin?.robust ?? props.altHash ?? props.address.hash }
text={ displayedAddress }
/>
);
});
......@@ -129,7 +136,7 @@ const Copy = (props: CopyProps) => {
return (
<EntityBase.Copy
{ ...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 { test, expect } from 'playwright/lib';
......@@ -18,7 +18,7 @@ const defaultProps = {
isRequired: true,
placeholder: 'Compiler',
name: 'compiler',
onChange: _noop,
onChange: noop,
};
[ 'md' as const, 'lg' as const ].forEach((size) => {
......
import type { HTMLChakraProps } 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 React from 'react';
......@@ -47,7 +47,7 @@ const CodeEditorSideBar = ({ onFileSelect, data, monaco, editor, selectedFile, m
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);
}, 100));
......
......@@ -367,7 +367,7 @@ describe('if there are multiple pages', () => {
describe('if there is page query param in URL', () => {
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'> = {
resourceName: 'address_txs',
......@@ -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() {
await flushPromises();
await act(flushPromises);
......
import type { UseQueryResult } 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 React, { useCallback } from 'react';
import { animateScroll } from 'react-scroll';
import type { PaginationParams } from './types';
import type { Route } from 'nextjs-routes';
import type { PaginatedResources, PaginationFilters, PaginationSorting, ResourceError, ResourcePayload } from 'lib/api/resources';
import { RESOURCES, SORTING_FIELDS } from 'lib/api/resources';
import type { Params as UseApiQueryParams } from 'lib/api/useApiQuery';
......@@ -26,6 +28,10 @@ type NextPageParams = Record<string, unknown>;
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) {
if (queryString) {
try {
......@@ -64,7 +70,7 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({
const queryClient = useQueryClient();
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>>({
[page]: getPaginationParamsFromQuery(router.query.next_page_params),
});
......@@ -148,7 +154,14 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({
}, [ queryClient, resourceName, router, scrollToTop ]);
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) {
Object.entries(newFilters).forEach(([ key, value ]) => {
const isValidValue = typeof value === 'boolean' || (value && value.length);
......@@ -173,8 +186,8 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({
}, [ router, resource, scrollToTop ]);
const onSortingChange = useCallback((newSorting: PaginationSorting<Resource> | undefined) => {
const newQuery = {
...omit<typeof router.query>(router.query, 'next_page_params', 'page', SORTING_FIELDS),
const newQuery: typeof router.query = {
...omit(router.query, [ 'next_page_params', 'page', ...SORTING_FIELDS ]),
...newSorting,
};
scrollToTop();
......@@ -221,5 +234,17 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({
}, 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 };
}
import _noop from 'lodash/noop';
import { noop } from 'es-toolkit';
import React from 'react';
import { test, expect } from 'playwright/lib';
......@@ -12,7 +12,7 @@ test('base view +@dark-mode', async({ render }) => {
<TagGroupSelect
items={ [ { id: '1', title: 'Option 1' }, { id: '2', title: 'Option 2' }, { id: 'duck', title: 'Cute little duck' } ] }
value="duck"
onChange={ _noop }
onChange={ noop }
/>,
);
......
import { Box, Select, VStack, Flex } from '@chakra-ui/react';
import capitalize from 'lodash/capitalize';
import { capitalize } from 'es-toolkit';
import React from 'react';
import type { NetworkGroup, FeaturedNetwork } from 'types/networks';
......
......@@ -8,7 +8,7 @@ import {
useDisclosure,
useOutsideClick,
} from '@chakra-ui/react';
import _debounce from 'lodash/debounce';
import { debounce } from 'es-toolkit';
import { useRouter } from 'next/router';
import type { FormEvent } from 'react';
import React from 'react';
......@@ -115,7 +115,7 @@ const SearchBar = ({ isHomepage }: Props) => {
}
calculateMenuWidth();
const resizeHandler = _debounce(calculateMenuWidth, 200);
const resizeHandler = debounce(calculateMenuWidth, 200);
const resizeObserver = new ResizeObserver(resizeHandler);
resizeObserver.observe(inputRef.current);
......
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 type { ChangeEvent, FormEvent, FocusEvent } from 'react';
......
import { Box, Tab, TabList, Tabs, Text, useColorModeValue } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query';
import throttle from 'lodash/throttle';
import { throttle } from 'es-toolkit';
import React from 'react';
import { scroller, Element } from 'react-scroll';
......
import _upperFirst from 'lodash/upperFirst';
import { upperFirst } from 'es-toolkit';
export function formatName(_name: string) {
const name = _name
.replaceAll('_', ' ')
.replaceAll(/\burl|nft|id\b/gi, (str) => str.toUpperCase());
return _upperFirst(name.trim());
return upperFirst(name.trim());
}
const PINNED_FIELDS = [ 'name', 'description' ];
......
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 type { PaginationParams } from 'ui/shared/pagination/types';
......@@ -34,7 +34,7 @@ export default function TxAssetFlows(props: FlowViewProps) {
const [ page, setPage ] = useState<number>(1);
const ViewData = useMemo(() => (queryData ? generateFlowViewData(queryData) : []), [ queryData ]);
const chunkedViewData = _chunk(ViewData, 50);
const chunkedViewData = chunk(ViewData, 50);
const paginationProps: PaginationParams = useMemo(() => ({
onNextPageClick: () => setPage(page + 1),
......
......@@ -94,7 +94,7 @@ const TxSubHeading = ({ hash, hasTag, txQuery }: Props) => {
return (
<TxInterpretation
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: {
sender_hash: {
type: 'address',
......
import _findIndex from 'lodash/findIndex';
import type { NovesNft, NovesResponseData, NovesSentReceived, NovesToken } from 'types/api/noves';
export interface NovesAction {
......@@ -27,7 +25,7 @@ export function generateFlowViewData(data: NovesResponseData): Array<NovesFlowVi
const txItems = [ ...sent, ...received ];
const paidGasIndex = _findIndex(txItems, (item) => item.action === 'paidGas');
const paidGasIndex = txItems.findIndex((item) => item.action === 'paidGas');
if (paidGasIndex >= 0) {
const element = txItems.splice(paidGasIndex, 1)[0];
element.to.name = 'Validators';
......
import _groupBy from 'lodash/groupBy';
import _keysIn from 'lodash/keysIn';
import _mapValues from 'lodash/mapValues';
import { groupBy, mapValues } from 'es-toolkit';
import type { NovesResponseData } from 'types/api/noves';
import type { TokenInfo } from 'types/api/token';
......@@ -49,28 +47,28 @@ export function getTokensData(data: NovesResponseData): TokensData {
});
// Group tokens by property into arrays
const tokensGroupByname = _groupBy(tokens, 'name');
const tokensGroupBySymbol = _groupBy(tokens, 'symbol');
const tokensGroupById = _groupBy(tokens, 'id');
const tokensGroupByName = groupBy(tokens, (item) => item.name || 'null');
const tokensGroupBySymbol = groupBy(tokens, (item) => item.symbol || 'null');
const tokensGroupById = groupBy(tokens, (item) => item.id || 'null');
// Map properties to an object and remove duplicates
const mappedNames = _mapValues(tokensGroupByname, (i) => {
const mappedNames = mapValues(tokensGroupByName, (i) => {
return i[0];
});
const mappedSymbols = _mapValues(tokensGroupBySymbol, (i) => {
const mappedSymbols = mapValues(tokensGroupBySymbol, (i) => {
return i[0];
});
const mappedIds = _mapValues(tokensGroupById, (i) => {
const mappedIds = mapValues(tokensGroupById, (i) => {
return i[0];
});
const filters = [ 'undefined', 'null' ];
// Array of keys to match in string
const nameList = _keysIn(mappedNames).filter(i => !filters.includes(i));
const symbolList = _keysIn(mappedSymbols).filter(i => !filters.includes(i));
const idList = _keysIn(mappedIds).filter(i => !filters.includes(i));
const nameList = Object.keys(mappedNames).filter(i => !filters.includes(i));
const symbolList = Object.keys(mappedSymbols).filter(i => !filters.includes(i));
const idList = Object.keys(mappedIds).filter(i => !filters.includes(i));
return {
nameList,
......
import { useQuery } from '@tanstack/react-query';
import _chunk from 'lodash/chunk';
import _uniq from 'lodash/uniq';
import { uniq, chunk } from 'es-toolkit';
import React from 'react';
import type { NovesDescribeTxsResponse } from 'types/api/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) {
const apiFetch = useApiFetch();
const txsHash = _uniq(items?.map(i => i.hash));
const txChunks = _chunk(txsHash, 10);
const txsHash = items ? uniq(items.map(i => i.hash)) : [];
const txChunks = chunk(txsHash, 10);
const queryKey = {
viewAsAccountAddress,
......
......@@ -2705,9 +2705,9 @@
integrity sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==
"@eslint/plugin-kit@^0.2.0":
version "0.2.2"
resolved "https://registry.yarnpkg.com/@eslint/plugin-kit/-/plugin-kit-0.2.2.tgz#5eff371953bc13e3f4d88150e2c53959f64f74f6"
integrity sha512-CXtq5nR4Su+2I47WPOlWud98Y5Lv8Kyxp2ukhgFx/eW6Blm18VXJO5WuQylPugRo8nbluoi6GvvxBLqHcvqUUw==
version "0.2.4"
resolved "https://registry.yarnpkg.com/@eslint/plugin-kit/-/plugin-kit-0.2.4.tgz#2b78e7bb3755784bb13faa8932a1d994d6537792"
integrity "sha1-K3jnuzdVeEuxP6qJMqHZlNZTd5I= sha512-zSkKow6H5Kdm0ZUQUB2kV5JIXqoG0+uH5YADhaEHswm664N9Db8dXSi0nMJpacpMf+MyyglF1vnZohpEg5yUtg=="
dependencies:
levn "^0.4.1"
......@@ -9180,7 +9180,7 @@ cross-fetch@^4.0.0:
dependencies:
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"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
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:
shebang-command "^2.0.0"
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:
version "4.2.0"
resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-4.2.0.tgz#4d931639ecdfd12ff80e8186dba6af2c2e856631"
......@@ -10039,9 +10048,9 @@ electron-to-chromium@^1.5.28:
integrity sha512-VufdJl+rzaKZoYVUijN13QcXVF5dWPZANeFTLNy+OSpHdDL5ynXTF35+60RSBbaQYB1ae723lQXHCrf4pyLsMw==
elliptic@^6.5.7:
version "6.5.7"
resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.7.tgz#8ec4da2cb2939926a1b9a73619d768207e647c8b"
integrity sha512-ESVCtTwiA+XhY3wyh24QqRGBoP3rEdDUl3EDUUo9tft074fi19IrdpH7hLCMMP3CIj7jb3W96rn8lt/BqIlt5Q==
version "6.6.1"
resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.6.1.tgz#3b8ffb02670bf69e382c7f65bf524c97c5405c06"
integrity "sha1-O4/7AmcL9p44LH9lv1JMl8VAXAY= sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g=="
dependencies:
bn.js "^4.11.9"
brorand "^1.1.0"
......@@ -10367,6 +10376,11 @@ es-to-primitive@^1.2.1:
is-date-object "^1.0.1"
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:
version "0.21.5"
resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.21.5.tgz#9ca301b120922959b766360d8ac830da0d02997d"
......@@ -13564,7 +13578,7 @@ lodash.throttle@^4.1.1:
resolved "https://registry.yarnpkg.com/lodash.throttle/-/lodash.throttle-4.1.1.tgz#c23e91b710242ac70c37f1e1cda9274cc39bf2f4"
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"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
......@@ -13944,9 +13958,9 @@ nan@^2.14.0, nan@^2.14.1, nan@^2.17.0:
integrity sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ==
nanoid@^3.3.6, nanoid@^3.3.7:
version "3.3.7"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8"
integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==
version "3.3.8"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.8.tgz#b1be3030bee36aaff18bacb375e5cce521684baf"
integrity "sha1-sb4wML7jaq/xi6yzdeXM5SFoS68= sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w=="
napi-build-utils@^1.0.1:
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