diff --git a/configs/essential-dapps-chains/config.edge.ts b/configs/essential-dapps-chains/config.edge.ts index 7686e63600..741f82cbd5 100644 --- a/configs/essential-dapps-chains/config.edge.ts +++ b/configs/essential-dapps-chains/config.edge.ts @@ -22,10 +22,14 @@ async function fetchConfig() { const url = baseUrl + '/assets/essential-dapps/chains.json'; const response = await fetch(url); - const json = await response.json(); - - value = json as MultichainConfig; - return value; + if (response.ok) { + const contentType = response.headers.get('content-type'); + if (contentType?.includes('application/json')) { + const json = await response.json(); + value = json as MultichainConfig; + return value; + } + } } export async function load() { diff --git a/configs/multichain/config.edge.ts b/configs/multichain/config.edge.ts index dbc97055a2..a88659bd96 100644 --- a/configs/multichain/config.edge.ts +++ b/configs/multichain/config.edge.ts @@ -22,10 +22,14 @@ async function fetchConfig() { const url = baseUrl + '/assets/multichain/config.json'; const response = await fetch(url); - const json = await response.json(); - - value = json as MultichainConfig; - return value; + if (response.ok) { + const contentType = response.headers.get('content-type'); + if (contentType?.includes('application/json')) { + const json = await response.json(); + value = json as MultichainConfig; + return value; + } + } } export async function load() { diff --git a/deploy/tools/essential-dapps-chains-config-generator/index.ts b/deploy/tools/essential-dapps-chains-config-generator/index.ts index 090a8bc9d7..a19e391af9 100644 --- a/deploy/tools/essential-dapps-chains-config-generator/index.ts +++ b/deploy/tools/essential-dapps-chains-config-generator/index.ts @@ -4,11 +4,10 @@ import { dirname, resolve as resolvePath } from 'node:path'; import { fileURLToPath } from 'node:url'; import { Worker } from 'node:worker_threads'; import * as viemChains from 'viem/chains'; -import { pick } from 'es-toolkit'; +import { pick, uniq, delay } from 'es-toolkit'; import { EssentialDappsConfig } from 'types/client/marketplace'; import { getEnvValue, parseEnvJson } from 'configs/app/utils'; -import { uniq } from 'es-toolkit'; import currentChainConfig from 'configs/app'; import appConfig from 'configs/app'; import { EssentialDappsChainConfig } from 'types/client/marketplace'; @@ -61,14 +60,15 @@ function trimChainConfig(config: typeof appConfig, logoUrl: string | undefined) } async function computeChainConfig(url: string): Promise { - return new Promise((resolve, reject) => { - const workerPath = resolvePath(currentDir, 'worker.js'); + const workerPath = resolvePath(currentDir, 'worker.js'); - const worker = new Worker(workerPath, { - workerData: { url }, - env: {} // Start with empty environment - }); + const worker = new Worker(workerPath, { + workerData: { url }, + env: {} // Start with empty environment + }); + const controller = new AbortController(); + const configPromise = new Promise((resolve, reject) => { worker.on('message', (config) => { resolve(config); }); @@ -79,11 +79,17 @@ async function computeChainConfig(url: string): Promise { }); worker.on('exit', (code) => { - if (code !== 0) { - reject(new Error(`Worker stopped with exit code ${ code }`)); - } + controller.abort(); + reject(new Error(`Worker stopped with exit code ${ code }`)); }); }); + + return Promise.race([ + configPromise, + delay(30_000, { signal: controller.signal }) + ]).finally(() => { + worker.terminate(); + }) } async function run() { @@ -116,7 +122,15 @@ async function run() { const explorerUrls = chainscoutInfo.externals.map(({ explorerUrl }) => explorerUrl).filter(Boolean); console.log(`ℹ️ For ${ explorerUrls.length } chains explorer url was found in static config. Fetching parameters for each chain...`); - const chainConfigs = await Promise.all(explorerUrls.map(computeChainConfig)) as Array; + const chainConfigs: Array = []; + + for (const explorerUrl of explorerUrls) { + const chainConfig = (await computeChainConfig(explorerUrl)) as typeof appConfig | undefined; + if (!chainConfig) { + throw new Error(`Failed to fetch chain config for ${ explorerUrl }`); + } + chainConfigs.push(chainConfig); + } const result = { chains: [ currentChainConfig, ...chainConfigs ].map((config) => { diff --git a/deploy/tools/multichain-config-generator/index.ts b/deploy/tools/multichain-config-generator/index.ts index ccd0b764dc..0dd75eb9a4 100644 --- a/deploy/tools/multichain-config-generator/index.ts +++ b/deploy/tools/multichain-config-generator/index.ts @@ -2,8 +2,10 @@ import { mkdirSync, writeFileSync } from 'node:fs'; import { dirname, resolve as resolvePath } from 'node:path'; import { fileURLToPath } from 'node:url'; import { Worker } from 'node:worker_threads'; +import { delay } from 'es-toolkit'; import { ClusterChainConfig } from 'types/multichain'; +import appConfig from 'configs/app'; const currentFilePath = fileURLToPath(import.meta.url); const currentDir = dirname(currentFilePath); @@ -48,14 +50,15 @@ async function getChainscoutInfo(chainIds: Array) { } async function computeChainConfig(url: string): Promise { - return new Promise((resolve, reject) => { - const workerPath = resolvePath(currentDir, 'worker.js'); + const workerPath = resolvePath(currentDir, 'worker.js'); - const worker = new Worker(workerPath, { - workerData: { url }, - env: {} // Start with empty environment - }); + const worker = new Worker(workerPath, { + workerData: { url }, + env: {} // Start with empty environment + }); + const controller = new AbortController(); + const configPromise = new Promise((resolve, reject) => { worker.on('message', (config) => { resolve(config); }); @@ -66,11 +69,18 @@ async function computeChainConfig(url: string): Promise { }); worker.on('exit', (code) => { - if (code !== 0) { - reject(new Error(`Worker stopped with exit code ${ code }`)); - } + controller.abort(); + reject(new Error(`Worker stopped with exit code ${ code }`)); }); + }); + + return Promise.race([ + configPromise, + delay(30_000, { signal: controller.signal }) + ]).finally(() => { + worker.terminate(); + }) } async function getExplorerUrls() { @@ -105,15 +115,27 @@ async function run() { throw new Error('No chains found in the cluster.'); } - const configs = await Promise.all(explorerUrls.map(computeChainConfig)); - const chainscoutInfo = await getChainscoutInfo(configs.map((config) => config.chain.id)); + const configs: Array = []; + for (const url of explorerUrls) { + const chainConfig = (await computeChainConfig(url)) as typeof appConfig | undefined; + if (!chainConfig) { + throw new Error(`Failed to fetch chain config for ${ url }`); + } + configs.push(chainConfig); + } + + const chainscoutInfo = await getChainscoutInfo( + configs + .map((config) => config.chain.id) + .filter((chainId) => chainId !== undefined) + ); const config = { chains: configs.map((config, index) => { const chainId = config.chain.id; const chainName = (config as { chain: { name: string } })?.chain?.name ?? `Chain ${ chainId }`; return { - id: chainId, + id: chainId || '', name: chainName, logo: chainscoutInfo.find((chain) => chain.id === chainId)?.logoUrl, explorer_url: explorerUrls[index], diff --git a/global.d.ts b/global.d.ts index 022033588e..e860a23270 100644 --- a/global.d.ts +++ b/global.d.ts @@ -22,8 +22,8 @@ declare global { }; abkw: string; __envs: Record; - __multichainConfig: MultichainConfig; - __essentialDappsChains: { chains: Array }; + __multichainConfig?: MultichainConfig; + __essentialDappsChains?: { chains: Array }; } namespace NodeJS { diff --git a/lib/errors/getErrorStack.ts b/lib/errors/getErrorStack.ts new file mode 100644 index 0000000000..2187c31d07 --- /dev/null +++ b/lib/errors/getErrorStack.ts @@ -0,0 +1,8 @@ +export default function getErrorStack(error: Error | undefined): string | undefined { + return ( + error && 'stack' in error && + typeof error.stack === 'string' && error.stack !== null && + error.stack + ) || + undefined; +} diff --git a/lib/rollbar/index.tsx b/lib/rollbar/index.tsx index 6019cc829e..41cd66fe8d 100644 --- a/lib/rollbar/index.tsx +++ b/lib/rollbar/index.tsx @@ -69,4 +69,6 @@ export const clientConfig: Configuration | undefined = feature.isEnabled ? { 'cancelled navigation', ], maxItems: 10, // Max items per page load + captureUncaught: true, + captureUnhandledRejections: true, } : undefined; diff --git a/package.json b/package.json index 027608c7a0..ec61a0a3fe 100644 --- a/package.json +++ b/package.json @@ -100,7 +100,7 @@ "magic-bytes.js": "1.8.0", "mixpanel-browser": "2.67.0", "monaco-editor": "^0.34.1", - "next": "15.5.9", + "next": "15.5.10", "next-themes": "0.4.4", "nextjs-routes": "^1.0.8", "node-fetch": "^3.2.9", diff --git a/pages/_error.tsx b/pages/_error.tsx index 7ca30c6908..d48cd9db32 100644 --- a/pages/_error.tsx +++ b/pages/_error.tsx @@ -1,10 +1,26 @@ +import type { NextPageContext } from 'next'; import NextErrorComponent from 'next/error'; import React from 'react'; +import Rollbar from 'rollbar'; import type { Props as ServerSidePropsCommon } from 'nextjs/getServerSideProps/handlers'; +import config from 'configs/app'; import * as cookies from 'lib/cookies'; +const rollbarFeature = config.features.rollbar; +const rollbar = rollbarFeature.isEnabled ? new Rollbar({ + accessToken: rollbarFeature.clientToken, + environment: rollbarFeature.environment, + payload: { + code_version: rollbarFeature.codeVersion, + app_instance: rollbarFeature.instance, + }, + maxItems: 10, + captureUncaught: true, + captureUnhandledRejections: true, +}) : undefined; + type Props = ServerSidePropsCommon & { statusCode: number; }; @@ -14,4 +30,25 @@ const CustomErrorComponent = (props: Props) => { return ; }; +CustomErrorComponent.getInitialProps = async(context: NextPageContext) => { + const { res, err, req } = context; + + const baseProps = await NextErrorComponent.getInitialProps(context); // Extract cookies from the request headers + const statusCode = res?.statusCode ?? err?.statusCode; + const cookies = req?.headers?.cookie || ''; + + if (rollbar) { + rollbar.error(err?.message ?? 'Unknown error', { + cause: err?.cause, + stack: err?.stack, + }); + } + + return { + ...baseProps, + statusCode, + cookies, + }; +}; + export default CustomErrorComponent; diff --git a/toolkit/theme/recipes/tag.recipe.ts b/toolkit/theme/recipes/tag.recipe.ts index 7654b855ca..b823780dd2 100644 --- a/toolkit/theme/recipes/tag.recipe.ts +++ b/toolkit/theme/recipes/tag.recipe.ts @@ -88,6 +88,7 @@ export const recipe = defineSlotRecipe({ px: '6px', py: '6px', minH: '8', + minW: '8', gap: '1', '--tag-avatar-size': 'spacing.4', '--tag-element-size': 'spacing.3', diff --git a/ui/address/AddressTokens.tsx b/ui/address/AddressTokens.tsx index 6da9c47ce9..d44c1056fe 100644 --- a/ui/address/AddressTokens.tsx +++ b/ui/address/AddressTokens.tsx @@ -47,13 +47,19 @@ const AddressTokens = ({ shouldRender = true, isQueryEnabled = true }: Props) => const tab = getQueryParamString(router.query.tab); const hash = getQueryParamString(router.query.hash); + // on address details we have tokens requests for all token types, separately + // react query can behave unexpectedly, when it already has data for ERC-20 (string type) + // and we fetch it again with the array type + // so if it's just one token type, we heed to keep it a string for queries compatibility + const tokenTypesFilter = config.chain.additionalTokenTypes.length > 0 ? [ 'ERC-20', ...config.chain.additionalTokenTypes.map(item => item.id) ] : 'ERC-20'; + const erc20Query = useQueryWithPages({ resourceName: 'general:address_tokens', pathParams: { hash }, - filters: { type: [ 'ERC-20', ...config.chain.additionalTokenTypes.map(item => item.id) ] }, + filters: { type: tokenTypesFilter }, scrollRef, options: { - enabled: isQueryEnabled && (!tab || tab === 'tokens' || tab === 'tokens_erc20'), + enabled: isQueryEnabled && (tab === 'tokens' || tab === 'tokens_erc20'), refetchOnMount: false, placeholderData: generateListStub<'general:address_tokens'>(ADDRESS_TOKEN_BALANCE_ERC_20, 10, { next_page_params: null }), }, diff --git a/ui/hotContracts/HotContractsIntervalSelect.tsx b/ui/hotContracts/HotContractsIntervalSelect.tsx index 864389b00c..03df177090 100644 --- a/ui/hotContracts/HotContractsIntervalSelect.tsx +++ b/ui/hotContracts/HotContractsIntervalSelect.tsx @@ -6,7 +6,6 @@ import type { HotContractsInterval } from 'types/api/contracts'; import useIsInitialLoading from 'lib/hooks/useIsInitialLoading'; import type { SelectOption } from 'toolkit/chakra/select'; import { Select } from 'toolkit/chakra/select'; -import type { TagProps } from 'toolkit/chakra/tag'; import TagGroupSelect from 'ui/shared/tagGroupSelect/TagGroupSelect'; import { INTERVAL_ITEMS } from './utils'; @@ -27,10 +26,9 @@ interface Props { interval: HotContractsInterval; onIntervalChange: (newInterval: HotContractsInterval) => void; isLoading?: boolean; - selectTagSize?: TagProps['size']; }; -const HotContractsIntervalSelect = ({ interval, onIntervalChange, isLoading, selectTagSize }: Props) => { +const HotContractsIntervalSelect = ({ interval, onIntervalChange, isLoading }: Props) => { const isInitialLoading = useIsInitialLoading(isLoading); @@ -44,7 +42,7 @@ const HotContractsIntervalSelect = ({ interval, onIntervalChange, isLoading, sel items={ intervalItems } onChange={ onIntervalChange } value={ interval } - tagSize={ selectTagSize } + tagSize="lg" loading={ isInitialLoading } disabled={ isLoading } hideBelow="lg" diff --git a/ui/hotContracts/HotContractsTableItem.tsx b/ui/hotContracts/HotContractsTableItem.tsx index 2fb02fb08f..1f3d2eb51e 100644 --- a/ui/hotContracts/HotContractsTableItem.tsx +++ b/ui/hotContracts/HotContractsTableItem.tsx @@ -26,7 +26,7 @@ const HotContractsTableItem = ({ return ( - + ) } - + - + - + { await checkAndSwitchChain(); - const activityResponse = await trackTransaction(address ?? '', transaction.to ?? ''); + const activityResponse = await trackTransaction( + address ?? '', + transaction.to ?? '', + isEssentialDapp ? String(chainId) : undefined, + ); const tx = await sendTransactionAsync(transaction); if (activityResponse?.token) { await trackTransactionConfirm(tx, activityResponse.token); } logEvent('Send Transaction'); return tx; - }, [ sendTransactionAsync, logEvent, trackTransaction, trackTransactionConfirm, address, checkAndSwitchChain ]); + }, [ + sendTransactionAsync, logEvent, trackTransaction, trackTransactionConfirm, + address, checkAndSwitchChain, chainId, isEssentialDapp, + ]); const signMessage = useCallback(async(message: string) => { await checkAndSwitchChain(); diff --git a/ui/megaEth/uptime/__screenshots__/Uptime.pw.tsx_default_default-view-1.png b/ui/megaEth/uptime/__screenshots__/Uptime.pw.tsx_default_default-view-1.png index e5beabeec3..b4d45c5df5 100644 Binary files a/ui/megaEth/uptime/__screenshots__/Uptime.pw.tsx_default_default-view-1.png and b/ui/megaEth/uptime/__screenshots__/Uptime.pw.tsx_default_default-view-1.png differ diff --git a/ui/megaEth/uptime/__screenshots__/Uptime.pw.tsx_default_default-view-2.png b/ui/megaEth/uptime/__screenshots__/Uptime.pw.tsx_default_default-view-2.png index 96a3fb3292..3c4c1c82a2 100644 Binary files a/ui/megaEth/uptime/__screenshots__/Uptime.pw.tsx_default_default-view-2.png and b/ui/megaEth/uptime/__screenshots__/Uptime.pw.tsx_default_default-view-2.png differ diff --git a/ui/pages/__screenshots__/HotContracts.pw.tsx_default_base-view-mobile-1.png b/ui/pages/__screenshots__/HotContracts.pw.tsx_default_base-view-mobile-1.png index 336f1ff113..a5bc680268 100644 Binary files a/ui/pages/__screenshots__/HotContracts.pw.tsx_default_base-view-mobile-1.png and b/ui/pages/__screenshots__/HotContracts.pw.tsx_default_base-view-mobile-1.png differ diff --git a/ui/shared/AppError/AppErrorBoundary.tsx b/ui/shared/AppError/AppErrorBoundary.tsx index db58b7164f..9c817df4bd 100644 --- a/ui/shared/AppError/AppErrorBoundary.tsx +++ b/ui/shared/AppError/AppErrorBoundary.tsx @@ -1,8 +1,11 @@ import { chakra } from '@chakra-ui/react'; import React from 'react'; +import getErrorCause from 'lib/errors/getErrorCause'; import getErrorCauseStatusCode from 'lib/errors/getErrorCauseStatusCode'; +import getErrorMessage from 'lib/errors/getErrorMessage'; import getErrorObjStatusCode from 'lib/errors/getErrorObjStatusCode'; +import getErrorStack from 'lib/errors/getErrorStack'; import { useRollbar } from 'lib/rollbar'; import ErrorBoundary from 'ui/shared/ErrorBoundary'; @@ -36,7 +39,10 @@ const AppErrorBoundary = ({ className, children, Container }: Props) => { // To this point, there can only be errors that lead to a page crash. // Therefore, we set the error level to "critical." - rollbar.critical(error); + rollbar.critical(getErrorMessage(error) ?? 'Application error', { + cause: getErrorCause(error), + stack: getErrorStack(error), + }); }, [ rollbar ]); return ( diff --git a/yarn.lock b/yarn.lock index ec14b33f1c..aa45668e50 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4519,10 +4519,10 @@ dependencies: webpack-bundle-analyzer "4.10.1" -"@next/env@15.5.9": - version "15.5.9" - resolved "https://registry.yarnpkg.com/@next/env/-/env-15.5.9.tgz#53c2c34dc17cd87b61f70c6cc211e303123b2ab8" - integrity sha512-4GlTZ+EJM7WaW2HEZcyU317tIQDjkQIyENDLxYJfSWlfqguN+dHkZgyQTV/7ykvobU7yEH5gKvreNrH4B6QgIg== +"@next/env@15.5.10": + version "15.5.10" + resolved "https://registry.yarnpkg.com/@next/env/-/env-15.5.10.tgz#3b0506c57d0977e60726a1663f36bc96d42c295b" + integrity sha512-plg+9A/KoZcTS26fe15LHg+QxReTazrIOoKKUC3Uz4leGGeNPgLHdevVraAAOX0snnUs3WkRx3eUQpj9mreG6A== "@next/eslint-plugin-next@15.0.3": version "15.0.3" @@ -16722,12 +16722,12 @@ next-themes@0.4.4: resolved "https://registry.yarnpkg.com/next-themes/-/next-themes-0.4.4.tgz#ce6f68a4af543821bbc4755b59c0d3ced55c2d13" integrity sha512-LDQ2qIOJF0VnuVrrMSMLrWGjRMkq+0mpgl6e0juCLqdJ+oo8Q84JRWT6Wh11VDQKkMMe+dVzDKLWs5n87T+PkQ== -next@15.5.9: - version "15.5.9" - resolved "https://registry.yarnpkg.com/next/-/next-15.5.9.tgz#1b80d05865cc27e710fb4dcfc6fd9e726ed12ad4" - integrity sha512-agNLK89seZEtC5zUHwtut0+tNrc0Xw4FT/Dg+B/VLEo9pAcS9rtTKpek3V6kVcVwsB2YlqMaHdfZL4eLEVYuCg== +next@15.5.10: + version "15.5.10" + resolved "https://registry.yarnpkg.com/next/-/next-15.5.10.tgz#5e3824d8f00dcd66ca4e79c38834f766976116bd" + integrity sha512-r0X65PNwyDDyOrWNKpQoZvOatw7BcsTPRKdwEqtc9cj3wv7mbBIk9tKed4klRaFXJdX0rugpuMTHslDrAU1bBg== dependencies: - "@next/env" "15.5.9" + "@next/env" "15.5.10" "@swc/helpers" "0.5.15" caniuse-lite "^1.0.30001579" postcss "8.4.31"