Merge pull request #2495 from blockscout/release/v1-37-2
Release v1.37.2
@@ -6,4 +6,8 @@ NEXT_PUBLIC_MIXPANEL_PROJECT_TOKEN=xxx
|
||||
NEXT_PUBLIC_GROWTH_BOOK_CLIENT_KEY=xxx
|
||||
NEXT_PUBLIC_AUTH0_CLIENT_ID=xxx
|
||||
NEXT_PUBLIC_GROWTH_BOOK_CLIENT_KEY=xxx
|
||||
NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_API_KEY=xxx
|
||||
NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_API_KEY=xxx
|
||||
|
||||
## DEPRECATED
|
||||
NEXT_PUBLIC_SENTRY_DSN=xxx
|
||||
NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY=xxx
|
||||
@@ -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',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Set of ENVs for Shibarium network explorer
|
||||
# https://www.shibariumscan.io
|
||||
# This is an auto-generated file. To update all values, run "yarn preset:sync --name=shibarium"
|
||||
# This is an auto-generated file. To update all values, run "yarn dev:preset:sync --name=shibarium"
|
||||
|
||||
# Local ENVs
|
||||
NEXT_PUBLIC_APP_PROTOCOL=http
|
||||
@@ -23,6 +23,7 @@ NEXT_PUBLIC_CONTRACT_CODE_IDES=[{'title':'Remix IDE','url':'https://remix.ethere
|
||||
NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info.services.blockscout.com
|
||||
NEXT_PUBLIC_DEFI_DROPDOWN_ITEMS=[{'text':'Get gas','icon':'gas','dappId':'smol-refuel'}]
|
||||
NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/shibarium-mainnet.json
|
||||
NEXT_PUBLIC_GAME_BADGE_CLAIM_LINK=https://badges.blockscout.com/mint/sherblockHolmesBadge
|
||||
NEXT_PUBLIC_GAS_REFUEL_PROVIDER_CONFIG={'name': 'Need gas?', 'url_template': 'https://smolrefuel.com/?outboundChain={chainId}&partner=blockscout&utm_source=blockscout&disableBridges=true', 'dapp_id': 'smol-refuel', 'logo': 'https://blockscout-content.s3.amazonaws.com/smolrefuel-logo-action-button.png'}
|
||||
NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0xce531d29c0c469fb00b443b8091b8c059b4f13d7e025dd0ef843401d02b9a1a9
|
||||
NEXT_PUBLIC_HAS_CONTRACT_AUDIT_REPORTS=true
|
||||
@@ -31,15 +32,17 @@ NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs', 'coin_price', 'market_cap']
|
||||
NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG={'background':['linear-gradient(180deg, rgba(224, 111, 44, 1) 0%, rgba(228, 144, 52, 1) 100%)'],'text_color':['rgba(255, 255, 255, 1)']}
|
||||
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
|
||||
NEXT_PUBLIC_LOGOUT_URL=https://shibarium.us.auth0.com/v2/logout
|
||||
NEXT_PUBLIC_MAINTENANCE_ALERT_MESSAGE=<p>Participated in our recent Blockscout activities? <a href="https://badges.blockscout.com?utm_source=instance&utm_medium=shibarium" target="_blank">Check your eligibility</a> and claim your NFT Scout badges. More exciting things are coming soon!</p>
|
||||
NEXT_PUBLIC_MAINTENANCE_ALERT_MESSAGE=<p>Joined recent campaigns? Mint your Merit Badge <a href="https://badges.blockscout.com?utm_source=instance&utm_medium=shibarium">here</a></p>
|
||||
NEXT_PUBLIC_MARKETPLACE_BANNER_CONTENT_URL=https://gist.githubusercontent.com/maikReal/974c47f86a3158c1a86b092ae2f044b3/raw/abcc7e02150cd85d4974503a0357162c0a2c35a9/merits-banner.html
|
||||
NEXT_PUBLIC_MARKETPLACE_BANNER_LINK_URL=https://swap.blockscout.com?utm_source=blockscout&utm_medium=shibarium
|
||||
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_RATING_AIRTABLE_API_KEY=patbqG4V2CI998jAq.9810c58c9de973ba2650621c94559088cbdfa1a914498e385621ed035d33c0d0
|
||||
NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_BASE_ID=appGkvtmKI7fXE4Vs
|
||||
NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-security-reports/default.json
|
||||
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/appiy5yijZpMMSKjT/shr6uMGPKjj1DK7NL
|
||||
NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM=https://airtable.com/appiy5yijZpMMSKjT/pag3t82DUCyhGRZZO/form
|
||||
NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com
|
||||
NEXT_PUBLIC_NAME_SERVICE_API_HOST=https://bens.services.blockscout.com
|
||||
NEXT_PUBLIC_NAVIGATION_HIGHLIGHTED_ROUTES=['/apps']
|
||||
NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18
|
||||
NEXT_PUBLIC_NETWORK_CURRENCY_NAME=BONE
|
||||
|
||||
@@ -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+ |
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
export const ABSENT_PARAM_ERROR_MESSAGE = 'Required param not provided';
|
||||
|
||||
export default function throwOnAbsentParamError(param: unknown) {
|
||||
if (!param) {
|
||||
throw new Error('Required param not provided', { cause: { status: 404 } });
|
||||
throw new Error(ABSENT_PARAM_ERROR_MESSAGE, { cause: { status: 404 } });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,11 +5,9 @@ import isNeedProxy from 'lib/api/isNeedProxy';
|
||||
import { getResourceKey } from 'lib/api/useApiQuery';
|
||||
import * as cookies from 'lib/cookies';
|
||||
import useFetch from 'lib/hooks/useFetch';
|
||||
import { useRollbar } from 'lib/rollbar';
|
||||
|
||||
export default function useGetCsrfToken() {
|
||||
const nodeApiFetch = useFetch();
|
||||
const rollbar = useRollbar();
|
||||
|
||||
return useQuery({
|
||||
queryKey: getResourceKey('csrf'),
|
||||
@@ -20,11 +18,13 @@ export default function useGetCsrfToken() {
|
||||
const csrfFromHeader = apiResponse.headers.get('x-bs-account-csrf');
|
||||
|
||||
if (!csrfFromHeader) {
|
||||
rollbar?.warn('Client fetch failed', {
|
||||
resource: 'csrf',
|
||||
status_code: 500,
|
||||
status_text: 'Unable to obtain csrf token from header',
|
||||
});
|
||||
// I am not sure should we log this error or not
|
||||
// so I commented it out for now
|
||||
// rollbar?.warn('Client fetch failed', {
|
||||
// resource: 'csrf',
|
||||
// status_code: 500,
|
||||
// status_text: 'Unable to obtain csrf token from header',
|
||||
// });
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,10 @@ import type React from 'react';
|
||||
import type { Configuration } from 'rollbar';
|
||||
|
||||
import config from 'configs/app';
|
||||
import { ABSENT_PARAM_ERROR_MESSAGE } from 'lib/errors/throwOnAbsentParamError';
|
||||
import { RESOURCE_LOAD_ERROR_MESSAGE } from 'lib/errors/throwOnResourceLoadError';
|
||||
|
||||
import { isBot, isHeadlessBrowser, isNextJsChunkError, getRequestInfo } from './utils';
|
||||
|
||||
const feature = config.features.rollbar;
|
||||
|
||||
@@ -20,4 +24,42 @@ export const clientConfig: Configuration | undefined = feature.isEnabled ? {
|
||||
code_version: feature.codeVersion,
|
||||
app_instance: feature.instance,
|
||||
},
|
||||
checkIgnore(isUncaught, args, item) {
|
||||
if (isBot(window.navigator.userAgent)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isHeadlessBrowser(window.navigator.userAgent)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isNextJsChunkError(getRequestInfo(item)?.url)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
hostSafeList: [ config.app.host ].filter(Boolean),
|
||||
ignoredMessages: [
|
||||
// these are React errors - "NotFoundError: Failed to execute 'removeChild' on 'Node': The node to be removed is not a child of this node."
|
||||
// they could be caused by browser extensions
|
||||
// one of the examples - https://github.com/facebook/react/issues/11538
|
||||
// we can ignore them for now
|
||||
'NotFoundError',
|
||||
|
||||
// these are errors that we throw on when make a call to the API
|
||||
RESOURCE_LOAD_ERROR_MESSAGE,
|
||||
ABSENT_PARAM_ERROR_MESSAGE,
|
||||
|
||||
// Filter out network-related errors that are usually not actionable
|
||||
'Network Error',
|
||||
'Failed to fetch',
|
||||
|
||||
// Filter out CORS errors from third-party extensions
|
||||
'cross-origin',
|
||||
|
||||
// Filter out client-side navigation cancellations
|
||||
'cancelled navigation',
|
||||
],
|
||||
maxItems: 10, // Max items per page load
|
||||
} : undefined;
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import type { Dictionary } from 'rollbar';
|
||||
|
||||
export function isBot(userAgent: string | undefined) {
|
||||
if (!userAgent) return false;
|
||||
|
||||
const botPatterns = [
|
||||
'Googlebot', // Google
|
||||
'Baiduspider', // Baidu
|
||||
'bingbot', // Bing
|
||||
'YandexBot', // Yandex
|
||||
'DuckDuckBot', // DuckDuckGo
|
||||
'Slurp', // Yahoo
|
||||
'Applebot', // Apple
|
||||
'facebookexternalhit', // Facebook
|
||||
'Twitterbot', // Twitter
|
||||
'rogerbot', // Moz
|
||||
'Alexa', // Alexa
|
||||
'AhrefsBot', // Ahrefs
|
||||
'SemrushBot', // Semrush
|
||||
'spider', // Generic spiders
|
||||
'crawler', // Generic crawlers
|
||||
];
|
||||
|
||||
return botPatterns.some(pattern =>
|
||||
userAgent.toLowerCase().includes(pattern.toLowerCase()),
|
||||
);
|
||||
}
|
||||
|
||||
export function isHeadlessBrowser(userAgent: string | undefined) {
|
||||
if (!userAgent) return false;
|
||||
|
||||
if (
|
||||
userAgent.includes('headless') ||
|
||||
userAgent.includes('phantomjs') ||
|
||||
userAgent.includes('selenium') ||
|
||||
userAgent.includes('puppeteer')
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export function isNextJsChunkError(url: unknown) {
|
||||
if (typeof url !== 'string') return false;
|
||||
return url.includes('/_next/');
|
||||
}
|
||||
|
||||
export function getRequestInfo(item: Dictionary): { url: string } | undefined {
|
||||
if (
|
||||
!item.request ||
|
||||
item.request === null ||
|
||||
typeof item.request !== 'object' ||
|
||||
!('url' in item.request) ||
|
||||
typeof item.request.url !== 'string'
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
return { url: item.request.url };
|
||||
}
|
||||
@@ -41,6 +41,7 @@ const LOCAL_ENVS = {
|
||||
const IGNORED_ENVS = [
|
||||
'NEXT_PUBLIC_GIT_COMMIT_SHA',
|
||||
'NEXT_PUBLIC_GIT_TAG',
|
||||
'NEXT_PUBLIC_ICON_SPRITE_HASH',
|
||||
];
|
||||
|
||||
function parseScriptArgs() {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type * as bens from '@blockscout/bens-types';
|
||||
import type { TokenType } from 'types/api/token';
|
||||
|
||||
export type SearchResultType = 'token' | 'address' | 'block' | 'transaction' | 'contract';
|
||||
@@ -47,6 +48,7 @@ export interface SearchResultDomain {
|
||||
expiry_date?: string;
|
||||
name: string;
|
||||
names_count: number;
|
||||
protocol?: bens.ProtocolInfo;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -118,8 +118,10 @@ 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 &&
|
||||
publicClient,
|
||||
|
||||
@@ -265,7 +265,7 @@ const SearchResultListItem = ({ data, searchTerm, isLoading, addressFormat }: Pr
|
||||
case 'ens_domain': {
|
||||
return (
|
||||
<EnsEntity.Container>
|
||||
<EnsEntity.Icon/>
|
||||
<EnsEntity.Icon protocol={ data.ens_info.protocol }/>
|
||||
<LinkInternal
|
||||
href={ route({ pathname: '/address/[hash]', query: { hash: data.address } }) }
|
||||
fontWeight={ 700 }
|
||||
|
||||
@@ -384,7 +384,7 @@ const SearchResultTableItem = ({ data, searchTerm, isLoading, addressFormat }: P
|
||||
<>
|
||||
<Td fontSize="sm">
|
||||
<EnsEntity.Container>
|
||||
<EnsEntity.Icon/>
|
||||
<EnsEntity.Icon protocol={ data.ens_info.protocol }/>
|
||||
<LinkInternal
|
||||
href={ route({ pathname: '/address/[hash]', query: { hash: data.address } }) }
|
||||
fontWeight={ 700 }
|
||||
|
||||
@@ -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': {
|
||||
|
||||
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 11 KiB |
@@ -7,11 +7,12 @@ import type { SearchResultDomain } from 'types/api/search';
|
||||
import { toBech32Address } from 'lib/address/bech32';
|
||||
import dayjs from 'lib/date/dayjs';
|
||||
import highlightText from 'lib/highlightText';
|
||||
import * as EnsEntity from 'ui/shared/entities/ens/EnsEntity';
|
||||
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
|
||||
import IconSvg from 'ui/shared/IconSvg';
|
||||
|
||||
const SearchBarSuggestDomain = ({ data, isMobile, searchTerm, addressFormat }: ItemsProps<SearchResultDomain>) => {
|
||||
const icon = <IconSvg name="ENS_slim" boxSize={ 5 } color="gray.500"/>;
|
||||
const icon = <EnsEntity.Icon protocol={ data.ens_info.protocol }/>;
|
||||
const hash = data.filecoin_robust_address || (addressFormat === 'bech32' ? toBech32Address(data.address) : data.address);
|
||||
|
||||
const name = (
|
||||
@@ -48,7 +49,7 @@ const SearchBarSuggestDomain = ({ data, isMobile, searchTerm, addressFormat }: I
|
||||
if (isMobile) {
|
||||
return (
|
||||
<>
|
||||
<Flex alignItems="center" overflow="hidden" gap={ 2 }>
|
||||
<Flex alignItems="center" overflow="hidden">
|
||||
{ icon }
|
||||
{ name }
|
||||
</Flex>
|
||||
@@ -63,7 +64,7 @@ const SearchBarSuggestDomain = ({ data, isMobile, searchTerm, addressFormat }: I
|
||||
|
||||
return (
|
||||
<Grid alignItems="center" gridTemplateColumns="228px minmax(auto, max-content) auto" gap={ 2 }>
|
||||
<Flex alignItems="center" gap={ 2 }>
|
||||
<Flex alignItems="center">
|
||||
{ icon }
|
||||
{ name }
|
||||
</Flex>
|
||||
|
||||