Commit 2a76c7cf authored by Max Alekseenko's avatar Max Alekseenko

Merge branch 'main' into ens-in-wallet-menu

parents 1ec77495 0684b062
name: Publish Chakra theme package to NPM
on:
workflow_dispatch:
inputs:
version:
description: Package version
type: string
required: true
workflow_call:
inputs:
version:
description: Package version
type: string
required: true
jobs:
publish:
runs-on: ubuntu-latest
name: Publish package to NPM registry
permissions:
id-token: write
steps:
- name: Checkout repo
uses: actions/checkout@v4
# Also it will setup .npmrc file to publish to npm
- name: Setup node
uses: actions/setup-node@v4
with:
node-version: '20.x'
registry-url: 'https://registry.npmjs.org'
- name: Update package version
run: |
cd ./theme
npm version ${{ inputs.version }}
- name: Build the package
run: |
cd ./theme
yarn
yarn build
- name: Publish to NPM registry
run: |
cd ./theme
npm publish --provenance --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
......@@ -11,6 +11,7 @@ on:
options:
- none
- arbitrum
- arbitrum_nova
- base
- celo_alfajores
- garnet
......
......@@ -11,6 +11,7 @@ on:
options:
- none
- arbitrum
- arbitrum_nova
- base
- celo_alfajores
- garnet
......
import type { RollupType } from 'types/client/rollup';
import type { NetworkVerificationType, NetworkVerificationTypeEnvs } from 'types/networks';
import { getEnvValue } from './utils';
const DEFAULT_CURRENCY_DECIMALS = 18;
const rollupType = getEnvValue('NEXT_PUBLIC_ROLLUP_TYPE') as RollupType;
const verificationType: NetworkVerificationType = (() => {
if (rollupType === 'arbitrum') {
return 'posting';
}
if (rollupType === 'zkEvm') {
return 'sequencing';
}
return getEnvValue('NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE') as NetworkVerificationTypeEnvs || 'mining';
})();
const chain = Object.freeze({
id: getEnvValue('NEXT_PUBLIC_NETWORK_ID'),
name: getEnvValue('NEXT_PUBLIC_NETWORK_NAME'),
......@@ -19,7 +34,7 @@ const chain = Object.freeze({
tokenStandard: getEnvValue('NEXT_PUBLIC_NETWORK_TOKEN_STANDARD_NAME') || 'ERC',
rpcUrl: getEnvValue('NEXT_PUBLIC_NETWORK_RPC_URL'),
isTestnet: getEnvValue('NEXT_PUBLIC_IS_TESTNET') === 'true',
verificationType: getEnvValue('NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE') || 'mining',
verificationType,
});
export default chain;
......@@ -5,7 +5,7 @@ export const replaceQuotes = (value: string | undefined) => value?.replaceAll('\
export const getEnvValue = (envName: string) => {
// eslint-disable-next-line no-restricted-properties
const envs = isBrowser() ? window.__envs : process.env;
const envs = (isBrowser() ? window.__envs : process.env) ?? {};
if (isBrowser() && envs.NEXT_PUBLIC_APP_INSTANCE === 'pw') {
const storageValue = localStorage.getItem(envName);
......
# Set of ENVs for Arbitrum One network explorer
# https://arbitrum.blockscout.com
# This is an auto-generated file. To update all values, run "yarn preset:sync --name=arbitrum"
# Local ENVs
NEXT_PUBLIC_APP_PROTOCOL=http
NEXT_PUBLIC_APP_HOST=localhost
NEXT_PUBLIC_APP_PORT=3000
NEXT_PUBLIC_APP_ENV=development
NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL=ws
# Instance ENVs
NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://admin-rs.services.blockscout.com
NEXT_PUBLIC_API_BASE_PATH=/
NEXT_PUBLIC_API_HOST=arbitrum-nova.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_GRAPHIQL_TRANSACTION=0x37c798810d49ba132b40efe7f4fdf6806a8fc58226bb5e185ddc91f896577abf
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs']
NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND=rgba(27, 74, 221, 1)
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
NEXT_PUBLIC_LOGOUT_URL=https://blockscout-arbitrum.us.auth0.com/v2/logout
NEXT_PUBLIC_MARKETPLACE_ENABLED=false
NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com
NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18
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':'/arbitrum/pools'}}]
NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/arbitrum-nova-icon.svg
NEXT_PUBLIC_NETWORK_ID=42170
NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/arbitrum-nova.svg
NEXT_PUBLIC_NETWORK_NAME=Arbitrum Nova
NEXT_PUBLIC_NETWORK_RPC_URL=https://arbitrum.llamarpc.com
NEXT_PUBLIC_NETWORK_SHORT_NAME=Arbitrum Nova
NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED=true
NEXT_PUBLIC_OG_IMAGE_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/og-images/arbitrum-nova.png
NEXT_PUBLIC_ROLLUP_L1_BASE_URL=https://eth.blockscout.com
NEXT_PUBLIC_ROLLUP_TYPE=arbitrum
NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout
NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=true
NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
\ No newline at end of file
......@@ -10,6 +10,8 @@ NEXT_PUBLIC_APP_ENV=development
NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL=ws
# Instance ENVs
NEXT_PUBLIC_VIEWS_CONTRACT_EXTRA_VERIFICATION_METHODS=none
NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://admin-rs.services.blockscout.com
NEXT_PUBLIC_API_BASE_PATH=/
NEXT_PUBLIC_API_HOST=zksync.blockscout.com
......
......@@ -33,7 +33,7 @@ releases:
type: kubernetes.io/dockerconfigjson
- name: bs-stack
chart: blockscout/blockscout-stack
version: 1.2.*
version: 1.*.*
namespace: review-l2-{{ requiredEnv "GITHUB_REF_NAME_SLUG" }}
labels:
app: review-l2-{{ requiredEnv "GITHUB_REF_NAME_SLUG" }}
......@@ -58,7 +58,7 @@ releases:
type: kubernetes.io/dockerconfigjson
- name: bs-stack
chart: blockscout/blockscout-stack
version: 1.2.*
version: 1.*.*
namespace: review-{{ requiredEnv "GITHUB_REF_NAME_SLUG" }}
labels:
app: review-{{ requiredEnv "GITHUB_REF_NAME_SLUG" }}
......
......@@ -31,7 +31,7 @@ import { SUPPORTED_WALLETS } from '../../../types/client/wallets';
import type { CustomLink, CustomLinksGroup } from '../../../types/footerLinks';
import { CHAIN_INDICATOR_IDS } from '../../../types/homepage';
import type { ChainIndicatorId } from '../../../types/homepage';
import { type NetworkVerificationType, type NetworkExplorer, type FeaturedNetwork, NETWORK_GROUPS } from '../../../types/networks';
import { type NetworkVerificationTypeEnvs, type NetworkExplorer, type FeaturedNetwork, NETWORK_GROUPS } from '../../../types/networks';
import { COLOR_THEME_IDS } from '../../../types/settings';
import type { AddressViewId } from '../../../types/views/address';
import { ADDRESS_VIEWS_IDS, IDENTICON_TYPES } from '../../../types/views/address';
......@@ -510,7 +510,17 @@ const schema = yup
NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS: yup.number().integer().positive(),
NEXT_PUBLIC_NETWORK_SECONDARY_COIN_SYMBOL: yup.string(),
NEXT_PUBLIC_NETWORK_MULTIPLE_GAS_CURRENCIES: yup.boolean(),
NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE: yup.string<NetworkVerificationType>().oneOf([ 'validation', 'mining' ]),
NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE: yup
.string<NetworkVerificationTypeEnvs>().oneOf([ 'validation', 'mining' ])
.when('NEXT_PUBLIC_ROLLUP_TYPE', {
is: (value: string) => value === 'arbitrum' || value === 'zkEvm',
then: (schema) => schema.test(
'not-exist',
'NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE can not be set for Arbitrum and ZkEVM rollups',
value => value === undefined,
),
otherwise: (schema) => schema,
}),
NEXT_PUBLIC_NETWORK_TOKEN_STANDARD_NAME: yup.string(),
NEXT_PUBLIC_IS_TESTNET: yup.boolean(),
......
......@@ -49,7 +49,6 @@ frontend:
env:
NEXT_PUBLIC_APP_ENV: development
NEXT_PUBLIC_APP_INSTANCE: review_L2
NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE: validation
NEXT_PUBLIC_NETWORK_LOGO: https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/base.svg
NEXT_PUBLIC_NETWORK_ICON: https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/base.svg
NEXT_PUBLIC_FEATURED_NETWORKS: https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/base-mainnet.json
......
......@@ -49,7 +49,6 @@ frontend:
env:
NEXT_PUBLIC_APP_ENV: development
NEXT_PUBLIC_APP_INSTANCE: review
NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE: validation
NEXT_PUBLIC_FEATURED_NETWORKS: https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/featured-networks/eth-sepolia.json
NEXT_PUBLIC_NETWORK_LOGO: https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/sepolia.svg
NEXT_PUBLIC_NETWORK_ICON: https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/sepolia.png
......@@ -61,18 +60,10 @@ frontend:
NEXT_PUBLIC_NAME_SERVICE_API_HOST: https://bens-rs-test.k8s-dev.blockscout.com
NEXT_PUBLIC_METADATA_SERVICE_API_HOST: https://metadata-test.k8s-dev.blockscout.com
NEXT_PUBLIC_AUTH_URL: https://blockscout-main.k8s-dev.blockscout.com
NEXT_PUBLIC_MARKETPLACE_ENABLED: true
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM: https://airtable.com/shrqUAcjgGJ4jU88C
NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM: https://airtable.com/appiy5yijZpMMSKjT/pag3t82DUCyhGRZZO/form
NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL: https://gist.githubusercontent.com/maxaleks/ce5c7e3de53e8f5b240b88265daf5839/raw/328383c958a8f7ecccf6d50c953bcdf8ab3faa0a/security_reports_goerli_test.json
NEXT_PUBLIC_LOGOUT_URL: https://blockscoutcom.us.auth0.com/v2/logout
NEXT_PUBLIC_HOMEPAGE_CHARTS: "['daily_txs','coin_price','market_cap']"
NEXT_PUBLIC_NETWORK_RPC_URL: https://eth-sepolia.public.blastapi.io
NEXT_PUBLIC_NETWORK_EXPLORERS: "[{'title':'Bitquery','baseUrl':'https://explorer.bitquery.io/','paths':{'tx':'/goerli/tx','address':'/goerli/address','token':'/goerli/token','block':'/goerli/block'}},{'title':'Etherscan','logo':'https://github.com/blockscout/frontend-configs/blob/main/configs/explorer-logos/etherscan.png?raw=true','baseUrl':'https://goerli.etherscan.io/','paths':{'tx':'/tx','address':'/address','token':'/token','block':'/block'}}]"
NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL: https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/marketplace-categories/default.json
NEXT_PUBLIC_MARKETPLACE_FEATURED_APP: zkbob-wallet
NEXT_PUBLIC_MARKETPLACE_BANNER_CONTENT_URL: https://gist.githubusercontent.com/maxaleks/36f779fd7d74877b57ec7a25a9a3a6c9/raw/746a8a59454c0537235ee44616c4690ce3bbf3c8/banner.html
NEXT_PUBLIC_MARKETPLACE_BANNER_LINK_URL: https://www.basename.app
NEXT_PUBLIC_GRAPHIQL_TRANSACTION: 0xf7d4972356e6ae44ae948d0cf19ef2beaf0e574c180997e969a2837da15e349d
NEXT_PUBLIC_WEB3_WALLETS: "['token_pocket','coinbase','metamask']"
NEXT_PUBLIC_VIEWS_ADDRESS_IDENTICON_TYPE: gradient_avatar
......@@ -86,6 +77,7 @@ frontend:
NEXT_PUBLIC_AD_BANNER_PROVIDER: slise
NEXT_PUBLIC_DATA_AVAILABILITY_ENABLED: true
NEXT_PUBLIC_NAVIGATION_HIGHLIGHTED_ROUTES: "['/apps']"
PROMETHEUS_METRICS_ENABLED: true
envFromSecret:
NEXT_PUBLIC_SENTRY_DSN: ref+vault://deployment-values/blockscout/dev/review?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_SENTRY_DSN
SENTRY_CSP_REPORT_URI: ref+vault://deployment-values/blockscout/dev/review?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/SENTRY_CSP_REPORT_URI
......@@ -96,4 +88,3 @@ frontend:
FAVICON_GENERATOR_API_KEY: ref+vault://deployment-values/blockscout/common?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_FAVICON_GENERATOR_API_KEY
NEXT_PUBLIC_GROWTH_BOOK_CLIENT_KEY: ref+vault://deployment-values/blockscout/dev/review?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_GROWTH_BOOK_CLIENT_KEY
NEXT_PUBLIC_MIXPANEL_PROJECT_TOKEN: ref+vault://deployment-values/blockscout/common?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_MIXPANEL_PROJECT_TOKEN
NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_API_KEY: ref+vault://deployment-values/blockscout/dev/common?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_API_KEY
......@@ -93,7 +93,7 @@ Please be aware that all environment variables prefixed with `NEXT_PUBLIC_` will
| NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS | `string` | Network currency decimals | - | `18` | `6` | v1.0.x+ |
| NEXT_PUBLIC_NETWORK_SECONDARY_COIN_SYMBOL | `string` | Network secondary coin symbol. | - | - | `GNO` | v1.29.0+ |
| NEXT_PUBLIC_NETWORK_MULTIPLE_GAS_CURRENCIES | `boolean` | Set to `true` for networks where users can pay transaction fees in either the native coin or ERC-20 tokens. | - | `false` | `true` | v1.33.0+ |
| NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE | `validation` or `mining` | Verification type in the network | - | `mining` | `validation` | v1.0.x+ |
| NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE | `validation` | `mining` | Verification type in the network. Irrelevant for Arbitrum (verification type is always `posting`) and ZkEvm (verification type is always `sequencing`) L2s | - | `mining` | `validation` | v1.0.x+ |
| NEXT_PUBLIC_NETWORK_TOKEN_STANDARD_NAME | `string` | Name of the standard for creating tokens | - | `ERC` | `BEP` | v1.31.0+ |
| NEXT_PUBLIC_IS_TESTNET | `boolean`| Set to true if network is testnet | - | `false` | `true` | v1.0.x+ |
......
......@@ -8,7 +8,7 @@ import React from 'react';
import { AppContextProvider } from 'lib/contexts/app';
import { ScrollDirectionProvider } from 'lib/contexts/scrollDirection';
import { SocketProvider } from 'lib/socket/context';
import theme from 'theme';
import theme from 'theme/theme';
import 'lib/setLocale';
......
......@@ -10,11 +10,12 @@ export default function buildUrl<R extends ResourceName>(
resourceName: R,
pathParams?: ResourcePathParams<R>,
queryParams?: Record<string, string | Array<string> | number | boolean | null | undefined>,
noProxy?: boolean,
): string {
const resource: ApiResource = RESOURCES[resourceName];
const baseUrl = isNeedProxy() ? config.app.baseUrl : (resource.endpoint || config.api.endpoint);
const baseUrl = !noProxy && isNeedProxy() ? config.app.baseUrl : (resource.endpoint || config.api.endpoint);
const basePath = resource.basePath !== undefined ? resource.basePath : config.api.basePath;
const path = isNeedProxy() ? '/node-api/proxy' + basePath + resource.path : basePath + resource.path;
const path = !noProxy && isNeedProxy() ? '/node-api/proxy' + basePath + resource.path : basePath + resource.path;
const url = new URL(compile(path)(pathParams), baseUrl);
queryParams && Object.entries(queryParams).forEach(([ key, value ]) => {
......
......@@ -43,10 +43,12 @@ import type { AddressesResponse } from 'types/api/addresses';
import type { AddressMetadataInfo, PublicTagTypesResponse } from 'types/api/addressMetadata';
import type {
ArbitrumL2MessagesResponse,
ArbitrumL2MessagesItem,
ArbitrumL2TxnBatch,
ArbitrumL2TxnBatchesResponse,
ArbitrumL2BatchTxs,
ArbitrumL2BatchBlocks,
ArbitrumL2TxnBatchesItem,
} from 'types/api/arbitrumL2';
import type { TxBlobs, Blob } from 'types/api/blobs';
import type { BlocksResponse, BlockTransactionsResponse, Block, BlockFilters, BlockWithdrawalsResponse, BlockCountdownResponse } from 'types/api/block';
......@@ -55,7 +57,6 @@ import type { BackendVersionConfig } from 'types/api/configs';
import type {
SmartContract,
SmartContractVerificationConfigRaw,
SolidityscanReport,
SmartContractSecurityAudits,
} from 'types/api/contract';
import type { VerifiedContractsResponse, VerifiedContractsFilters, VerifiedContractsCounters } from 'types/api/contracts';
......@@ -484,7 +485,7 @@ export const RESOURCES = {
path: '/api/v2/smart-contracts/:hash/verification/via/:method',
pathParams: [ 'hash' as const, 'method' as const ],
},
contract_solidityscan_report: {
contract_solidity_scan_report: {
path: '/api/v2/smart-contracts/:hash/solidityscan-report',
pathParams: [ 'hash' as const ],
},
......@@ -586,15 +587,21 @@ export const RESOURCES = {
homepage_blocks: {
path: '/api/v2/main-page/blocks',
},
homepage_deposits: {
homepage_optimistic_deposits: {
path: '/api/v2/main-page/optimism-deposits',
},
homepage_arbitrum_deposits: {
path: '/api/v2/main-page/arbitrum/messages/to-rollup',
},
homepage_txs: {
path: '/api/v2/main-page/transactions',
},
homepage_zkevm_l2_batches: {
path: '/api/v2/main-page/zkevm/batches/confirmed',
},
homepage_arbitrum_l2_batches: {
path: '/api/v2/main-page/arbitrum/batches/committed',
},
homepage_txs_watchlist: {
path: '/api/v2/main-page/transactions/watchlist',
},
......@@ -607,6 +614,9 @@ export const RESOURCES = {
homepage_zksync_latest_batch: {
path: '/api/v2/main-page/zksync/batches/latest-number',
},
homepage_arbitrum_latest_batch: {
path: '/api/v2/main-page/arbitrum/batches/latest-number',
},
// SEARCH
quick_search: {
......@@ -972,11 +982,14 @@ Q extends 'stats_charts_secondary_coin_price' ? ChartSecondaryCoinPriceResponse
Q extends 'homepage_blocks' ? Array<Block> :
Q extends 'homepage_txs' ? Array<Transaction> :
Q extends 'homepage_txs_watchlist' ? Array<Transaction> :
Q extends 'homepage_deposits' ? Array<OptimisticL2DepositsItem> :
Q extends 'homepage_optimistic_deposits' ? Array<OptimisticL2DepositsItem> :
Q extends 'homepage_arbitrum_deposits' ? { items: Array<ArbitrumL2MessagesItem> } :
Q extends 'homepage_zkevm_l2_batches' ? { items: Array<ZkEvmL2TxnBatchesItem> } :
Q extends 'homepage_arbitrum_l2_batches' ? { items: Array<ArbitrumL2TxnBatchesItem>} :
Q extends 'homepage_indexing_status' ? IndexingStatus :
Q extends 'homepage_zkevm_latest_batch' ? number :
Q extends 'homepage_zksync_latest_batch' ? number :
Q extends 'homepage_arbitrum_latest_batch' ? number :
Q extends 'stats_counters' ? stats.Counters :
Q extends 'stats_lines' ? stats.LineCharts :
Q extends 'stats_line' ? stats.LineChart :
......@@ -1030,13 +1043,11 @@ Q extends 'quick_search' ? Array<SearchResultItem> :
Q extends 'search' ? SearchResult :
Q extends 'search_check_redirect' ? SearchRedirectResult :
Q extends 'contract' ? SmartContract :
Q extends 'contract_solidityscan_report' ? SolidityscanReport :
Q extends 'contract_solidity_scan_report' ? unknown :
Q extends 'verified_contracts' ? VerifiedContractsResponse :
Q extends 'verified_contracts_counters' ? VerifiedContractsCounters :
Q extends 'visualize_sol2uml' ? visualizer.VisualizeResponse :
Q extends 'contract_verification_config' ? SmartContractVerificationConfigRaw :
Q extends 'withdrawals' ? WithdrawalsResponse :
Q extends 'withdrawals_counters' ? WithdrawalsCounters :
Q extends 'optimistic_l2_output_roots' ? OptimisticL2OutputRootsResponse :
Q extends 'optimistic_l2_withdrawals' ? OptimisticL2WithdrawalsResponse :
Q extends 'optimistic_l2_deposits' ? OptimisticL2DepositsResponse :
......@@ -1104,6 +1115,8 @@ Q extends 'address_mud_tables' ? AddressMudTables :
Q extends 'address_mud_tables_count' ? number :
Q extends 'address_mud_records' ? AddressMudRecords :
Q extends 'address_mud_record' ? AddressMudRecord :
Q extends 'withdrawals' ? WithdrawalsResponse :
Q extends 'withdrawals_counters' ? WithdrawalsCounters :
never;
/* eslint-enable @typescript-eslint/indent */
......
import type { UseQueryOptions } from '@tanstack/react-query';
import type { QueryKey, UseQueryOptions } from '@tanstack/react-query';
import { useQuery } from '@tanstack/react-query';
import type { ResourceError, ResourceName, ResourcePayload } from './resources';
import type { Params as ApiFetchParams } from './useApiFetch';
import type { Params as FetchParams } from 'lib/hooks/useFetch';
import type { ResourceError, ResourceName, ResourcePathParams, ResourcePayload } from './resources';
import useApiFetch from './useApiFetch';
export interface Params<R extends ResourceName, E = unknown, D = ResourcePayload<R>> extends ApiFetchParams<R> {
export interface Params<R extends ResourceName, E = unknown, D = ResourcePayload<R>> {
pathParams?: ResourcePathParams<R>;
queryParams?: Record<string, string | Array<string> | number | boolean | undefined>;
fetchParams?: Pick<FetchParams, 'body' | 'method' | 'headers'>;
queryOptions?: Omit<UseQueryOptions<ResourcePayload<R>, ResourceError<E>, D>, 'queryKey' | 'queryFn'>;
queryKey?: QueryKey;
}
export function getResourceKey<R extends ResourceName>(resource: R, { pathParams, queryParams }: Params<R> = {}) {
......@@ -19,18 +24,18 @@ export function getResourceKey<R extends ResourceName>(resource: R, { pathParams
export default function useApiQuery<R extends ResourceName, E = unknown, D = ResourcePayload<R>>(
resource: R,
{ queryOptions, pathParams, queryParams, fetchParams }: Params<R, E, D> = {},
{ queryOptions, pathParams, queryParams, queryKey, fetchParams }: Params<R, E, D> = {},
) {
const apiFetch = useApiFetch();
return useQuery<ResourcePayload<R>, ResourceError<E>, D>({
// eslint-disable-next-line @tanstack/query/exhaustive-deps
queryKey: getResourceKey(resource, { pathParams, queryParams }),
queryFn: async() => {
queryKey: queryKey || getResourceKey(resource, { pathParams, queryParams }),
queryFn: async({ signal }) => {
// all errors and error typing is handled by react-query
// so error response will never go to the data
// that's why we are safe here to do type conversion "as Promise<ResourcePayload<R>>"
return apiFetch(resource, { pathParams, queryParams, fetchParams }) as Promise<ResourcePayload<R>>;
return apiFetch(resource, { pathParams, queryParams, fetchParams: { ...fetchParams, signal } }) as Promise<ResourcePayload<R>>;
},
...queryOptions,
});
......
......@@ -6,7 +6,7 @@ import {
import type { ChakraProviderProps } from '@chakra-ui/react';
import React from 'react';
import theme from 'theme';
import theme from 'theme/theme';
interface Props extends ChakraProviderProps {
cookies?: string;
......
......@@ -57,11 +57,11 @@ export default function useNewTxsSocket() {
}, [ setNum ]);
const handleSocketClose = React.useCallback(() => {
setSocketAlert('Connection is lost. Please reload page.');
setSocketAlert('Connection is lost. Please reload the page.');
}, []);
const handleSocketError = React.useCallback(() => {
setSocketAlert('An error has occurred while fetching new transactions. Please reload page.');
setSocketAlert('An error has occurred while fetching new transactions. Please reload the page.');
}, []);
const channel = useSocketChannel({
......
......@@ -19,7 +19,7 @@ function getUnits(diff: number) {
return [ DAY, 2 * DAY ];
}
function getUpdateParams(ts: string) {
function getUpdateParams(ts: string | number) {
const timeDiff = Date.now() - new Date(ts).getTime();
const [ unit, higherUnit ] = getUnits(timeDiff);
......@@ -41,7 +41,7 @@ function getUpdateParams(ts: string) {
};
}
export default function useTimeAgoIncrement(ts: string | null, isEnabled?: boolean) {
export default function useTimeAgoIncrement(ts: string | number | null, isEnabled?: boolean) {
const [ value, setValue ] = React.useState(ts ? dayjs(ts).fromNow() : null);
React.useEffect(() => {
......
......@@ -54,6 +54,7 @@ const OG_TYPE_DICT: Record<Route['pathname'], OGPageType> = {
'/login': 'Regular page',
'/sprite': 'Regular page',
'/api/metrics': 'Regular page',
'/api/monitoring/invalid-api-schema': 'Regular page',
'/api/log': 'Regular page',
'/api/media-type': 'Regular page',
'/api/proxy': 'Regular page',
......
......@@ -58,6 +58,7 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
'/login': DEFAULT_TEMPLATE,
'/sprite': DEFAULT_TEMPLATE,
'/api/metrics': DEFAULT_TEMPLATE,
'/api/monitoring/invalid-api-schema': DEFAULT_TEMPLATE,
'/api/log': DEFAULT_TEMPLATE,
'/api/media-type': DEFAULT_TEMPLATE,
'/api/proxy': DEFAULT_TEMPLATE,
......
......@@ -54,6 +54,7 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
'/login': '%network_name% login',
'/sprite': '%network_name% SVG sprite',
'/api/metrics': '%network_name% node API prometheus metrics',
'/api/monitoring/invalid-api-schema': '%network_name% node API prometheus metrics',
'/api/log': '%network_name% node API request log',
'/api/media-type': '%network_name% node API media type',
'/api/proxy': '%network_name% node API proxy',
......
......@@ -52,6 +52,7 @@ export const PAGE_TYPE_DICT: Record<Route['pathname'], string> = {
'/login': 'Login',
'/sprite': 'Sprite',
'/api/metrics': 'Node API: Prometheus metrics',
'/api/monitoring/invalid-api-schema': 'Node API: Prometheus metrics',
'/api/log': 'Node API: Request log',
'/api/media-type': 'Node API: Media type',
'/api/proxy': 'Node API: Proxy',
......
......@@ -8,6 +8,12 @@ const metrics = (() => {
promClient.register.clear();
const invalidApiSchema = new promClient.Counter({
name: 'invalid_api_schema',
help: 'Number of invalid external API schema events',
labelNames: [ 'resource', 'url' ] as const,
});
const socialPreviewBotRequests = new promClient.Counter({
name: 'social_preview_bot_requests_total',
help: 'Number of incoming requests from social preview bots',
......@@ -27,7 +33,7 @@ const metrics = (() => {
buckets: [ 0.2, 0.5, 1, 3, 10 ],
});
return { socialPreviewBotRequests, searchEngineBotRequests, apiRequestDuration };
return { invalidApiSchema, socialPreviewBotRequests, searchEngineBotRequests, apiRequestDuration };
})();
export default metrics;
import config from 'configs/app';
export default function getNetworkValidationActionText() {
switch (config.chain.verificationType) {
case 'validation': {
return 'validated';
}
case 'mining': {
return 'mined';
}
case 'posting': {
return 'posted';
}
case 'sequencing': {
return 'sequenced';
}
default: {
return 'miner';
}
}
}
import config from 'configs/app';
export default function getNetworkValidatorTitle() {
return config.chain.verificationType === 'validation' ? 'validator' : 'miner';
switch (config.chain.verificationType) {
case 'validation': {
return 'validator';
}
case 'mining': {
return 'miner';
}
case 'posting': {
return 'poster';
}
case 'sequencing': {
return 'sequencer';
}
default: {
return 'miner';
}
}
}
import type { Channel } from 'phoenix';
import type { AddressCoinBalanceHistoryItem, AddressTokensBalancesSocketMessage } from 'types/api/address';
import type { NewArbitrumBatchSocketResponse } from 'types/api/arbitrumL2';
import type { NewBlockSocketResponse } from 'types/api/block';
import type { SmartContractVerificationResponse } from 'types/api/contract';
import type { RawTracesResponse } from 'types/api/rawTrace';
......@@ -16,7 +17,8 @@ SocketMessage.TxStatusUpdate |
SocketMessage.TxRawTrace |
SocketMessage.NewTx |
SocketMessage.NewPendingTx |
SocketMessage.NewDeposits |
SocketMessage.NewOptimisticDeposits |
SocketMessage.NewArbitrumDeposits |
SocketMessage.AddressBalance |
SocketMessage.AddressCurrentCoinBalance |
SocketMessage.AddressTokenBalance |
......@@ -36,6 +38,7 @@ SocketMessage.TokenTotalSupply |
SocketMessage.TokenInstanceMetadataFetched |
SocketMessage.ContractVerification |
SocketMessage.NewZkEvmL2Batch |
SocketMessage.NewArbitrumL2Batch |
SocketMessage.Unknown;
interface SocketMessageParamsGeneric<Event extends string | undefined, Payload extends object | unknown> {
......@@ -53,7 +56,8 @@ export namespace SocketMessage {
export type TxRawTrace = SocketMessageParamsGeneric<'raw_trace', RawTracesResponse>;
export type NewTx = SocketMessageParamsGeneric<'transaction', { transaction: number }>;
export type NewPendingTx = SocketMessageParamsGeneric<'pending_transaction', { pending_transaction: number }>;
export type NewDeposits = SocketMessageParamsGeneric<'deposits', { deposits: number }>;
export type NewOptimisticDeposits = SocketMessageParamsGeneric<'deposits', { deposits: number }>;
export type NewArbitrumDeposits = SocketMessageParamsGeneric<'new_messages_to_rollup_amount', { new_messages_to_rollup_amount: number }>;
export type AddressBalance = SocketMessageParamsGeneric<'balance', { balance: string; block_number: number; exchange_rate: string }>;
export type AddressCurrentCoinBalance =
SocketMessageParamsGeneric<'current_coin_balance', { coin_balance: string; block_number: number; exchange_rate: string }>;
......@@ -74,5 +78,6 @@ export namespace SocketMessage {
export type TokenInstanceMetadataFetched = SocketMessageParamsGeneric<'fetched_token_instance_metadata', TokenInstanceMetadataSocketMessage>;
export type ContractVerification = SocketMessageParamsGeneric<'verification_result', SmartContractVerificationResponse>;
export type NewZkEvmL2Batch = SocketMessageParamsGeneric<'new_zkevm_confirmed_batch', NewZkEvmBatchSocketResponse>;
export type NewArbitrumL2Batch = SocketMessageParamsGeneric<'new_arbitrum_batch', NewArbitrumBatchSocketResponse>;
export type Unknown = SocketMessageParamsGeneric<undefined, unknown>;
}
import * as v from 'valibot';
export const SolidityScanIssueSeverityDistributionSchema = v.object({
critical: v.number(),
gas: v.number(),
high: v.number(),
informational: v.number(),
low: v.number(),
medium: v.number(),
});
export const SolidityScanSchema = v.object({
scan_report: v.object({
contractname: v.string(),
scan_status: v.string(),
scan_summary: v.object({
score_v2: v.string(),
issue_severity_distribution: SolidityScanIssueSeverityDistributionSchema,
}),
scanner_reference_url: v.string(),
}),
});
export type SolidityScanReport = v.InferOutput<typeof SolidityScanSchema>;
export type SolidityScanReportSeverityDistribution = v.InferOutput<typeof SolidityScanIssueSeverityDistributionSchema>;
import React from 'react';
import * as v from 'valibot';
import buildUrl from 'lib/api/buildUrl';
import useApiQuery from 'lib/api/useApiQuery';
import { SOLIDITY_SCAN_REPORT } from 'stubs/contract';
import { SolidityScanSchema } from './schema';
interface Params {
hash: string;
}
const RESOURCE_NAME = 'contract_solidity_scan_report';
const ERROR_NAME = 'Invalid response schema';
export default function useFetchReport({ hash }: Params) {
const query = useApiQuery(RESOURCE_NAME, {
pathParams: { hash },
queryOptions: {
select: (response) => {
const parsedResponse = v.safeParse(SolidityScanSchema, response);
if (!parsedResponse.success) {
throw Error(ERROR_NAME);
}
return parsedResponse.output;
},
enabled: Boolean(hash),
placeholderData: SOLIDITY_SCAN_REPORT,
retry: 0,
},
});
const errorMessage = query.error && 'message' in query.error ? query.error.message : undefined;
React.useEffect(() => {
if (errorMessage === ERROR_NAME) {
fetch('/node-api/monitoring/invalid-api-schema', {
method: 'POST',
body: JSON.stringify({
resource: RESOURCE_NAME,
url: buildUrl(RESOURCE_NAME, { hash }, undefined, true),
}),
});
}
}, [ errorMessage, hash ]);
return query;
}
/* eslint-disable max-len */
import type { ArbitrumMessageStatus } from 'types/api/transaction';
export const MESSAGE_DESCRIPTIONS: Record<ArbitrumMessageStatus, string> = {
'Syncing with base layer': 'The incoming message was discovered on the rollup, but the corresponding message on L1 has not yet been found',
'Settlement pending': 'The transaction with the message was included in a rollup block, but there is no batch on L1 containing the block yet',
'Waiting for confirmation': 'The rollup block with the transaction containing the message was included in a batch on L1, but it is still waiting for the expiration of the fraud proof countdown',
'Ready for relay': 'The rollup state was confirmed successfully, and the message can be executed—funds can be claimed on L1',
Relayed: '',
};
/* eslint-disable max-len */
import type { ArbitrumL2TxnBatch } from 'types/api/arbitrumL2';
import { finalized } from './txnBatches';
......@@ -8,4 +9,32 @@ export const batchData: ArbitrumL2TxnBatch = {
before_acc: '0x2ed7c4985eb778d76ec400a43805e7feecc8c2afcdb492dbe5caf227de6d37bc',
start_block: 1245209,
end_block: 1245490,
data_availability: {
batch_data_container: 'in_blob4844',
},
};
export const batchDataAnytrust: ArbitrumL2TxnBatch = {
...finalized,
after_acc: '0xcd064f3409015e8e6407e492e5275a185e492c6b43ccf127f22092d8057a9ffb',
before_acc: '0x2ed7c4985eb778d76ec400a43805e7feecc8c2afcdb492dbe5caf227de6d37bc',
start_block: 1245209,
end_block: 1245490,
data_availability: {
batch_data_container: 'in_anytrust',
bls_signature: '0x142577943e30b1ad1b4e40a1c08e00c24a68d6c366f953e361048b7127e327b5bdb8f168ba986beae40cfaf79ea2788004d750555684751e361d6f6445e5c521b45ac93a76da24add241a4a5410ca3a09fa82cf0aafd78801cbd0ad99d5be6b3',
data_hash: '0x4ffada101d8185bcba227f2cff9e0ea0a4deeb08f328601a898131429a436ebe',
timeout: '2024-08-22T12:39:22Z',
signers: [
{
key: '0x0c6694955b524d718ca445831c5375393773401f33725a79661379dddabd5fff28619dc070befd9ed73d699e5c236c1a163be58ba81002b6130709bc064af5d7ba947130b72056bf17263800f1a3ab2269c6a510ef8e7412fd56d1ef1b916a1306e3b1d9c82c099371bd9861582acaada3a16e9dfee5d0ebce61096598a82f112d0a935e8cab5c48d82e3104b0c7ba79157dad1a019a3e7f6ad077b8e6308b116fec0f58239622463c3631fa01e2b4272409215b8009422c16715dbede590906',
proof: '0x06dcb5e56764bb72e6a45e6deb301ca85d8c4315c1da2efa29927f2ac8fb25571ce31d2d603735fe03196f6d56bcbf9a1999a89a74d5369822c4445d676c15ed52e5008daa775dc9a839c99ff963a19946ac740579874dac4f639907ae1bc69f',
trusted: false,
},
{
key: '0x0ee5aaeabd57313285207eb89366b411286cf3f1c5e30eb7e355f55385308b91d5807284323ee89a9743c70676f4949504ced3ed41612cbfda06ad55200c1c77d3fb3700059befd64c44bc4a57cb567ec1481ee564cf6cd6cf1f2f4a2dee6db00c547c38400ab118dedae8afd5bab93b703f76a0991baa5d43fbb125194c06b5461f8c738a3c4278a3d98e5456aec0720883c0d28919537a36e2ffd5f731e742b6653557d154c164e068ef983b367ef626faaed46f4eadecbb12b7e55f23175d',
trusted: true,
},
],
},
};
......@@ -10,6 +10,7 @@ export const finalized: ArbitrumL2TxnBatchesItem = {
hash: '0x262e7215739d6a7e33b2c20b45a838801a0f5f080f20bec8e54eb078420c4661',
status: 'finalized',
},
batch_data_container: 'in_blob4844',
};
export const unfinalized: ArbitrumL2TxnBatchesItem = {
......@@ -22,6 +23,8 @@ export const unfinalized: ArbitrumL2TxnBatchesItem = {
hash: '0x262e7215739d6a7e33b2c20b45a838801a0f5f080f20bec8e54eb078420c4661',
status: 'unfinalized',
},
batch_data_container: null,
};
export const baseResponse: ArbitrumL2TxnBatchesResponse = {
......
......@@ -99,6 +99,13 @@ export const withChangedByteCode: SmartContract = {
is_blueprint: true,
};
export const zkSync: SmartContract = {
...verified,
zk_compiler_version: 'v1.2.5',
optimization_enabled: true,
optimization_runs: 's',
};
export const nonVerified: SmartContract = {
is_verified: false,
is_blueprint: false,
......
import type { SolidityscanReport } from 'types/api/contract';
import type { SolidityScanReport } from 'lib/solidityScan/schema';
export const solidityscanReportAverage: SolidityscanReport = {
export const solidityscanReportAverage: SolidityScanReport = {
scan_report: {
contractname: 'foo',
scan_status: 'scan_done',
......@@ -13,17 +13,13 @@ export const solidityscanReportAverage: SolidityscanReport = {
low: 2,
medium: 0,
},
lines_analyzed_count: 18,
scan_time_taken: 1,
score: '3.61',
score_v2: '72.22',
threat_score: '94.74',
},
scanner_reference_url: 'https://solidityscan.com/quickscan/0xc1EF7811FF2ebFB74F80ed7423f2AdAA37454be2/blockscout/eth-goerli?ref=blockscout',
},
};
export const solidityscanReportGreat: SolidityscanReport = {
export const solidityscanReportGreat: SolidityScanReport = {
scan_report: {
contractname: 'foo',
scan_status: 'scan_done',
......@@ -36,17 +32,13 @@ export const solidityscanReportGreat: SolidityscanReport = {
low: 0,
medium: 0,
},
lines_analyzed_count: 18,
scan_time_taken: 1,
score: '3.61',
score_v2: '100',
threat_score: '94.74',
},
scanner_reference_url: 'https://solidityscan.com/quickscan/0xc1EF7811FF2ebFB74F80ed7423f2AdAA37454be2/blockscout/eth-goerli?ref=blockscout',
},
};
export const solidityscanReportLow: SolidityscanReport = {
export const solidityscanReportLow: SolidityScanReport = {
scan_report: {
contractname: 'foo',
scan_status: 'scan_done',
......@@ -59,11 +51,7 @@ export const solidityscanReportLow: SolidityscanReport = {
low: 2,
medium: 10,
},
lines_analyzed_count: 18,
scan_time_taken: 1,
score: '3.61',
score_v2: '22.22',
threat_score: '94.74',
},
scanner_reference_url: 'https://solidityscan.com/quickscan/0xc1EF7811FF2ebFB74F80ed7423f2AdAA37454be2/blockscout/eth-goerli?ref=blockscout',
},
......
......@@ -63,6 +63,14 @@ export const ensDomainA: bens.DetailedDomain = {
NEAR: 'a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48.factory.bridge.near',
},
protocol: protocolA,
resolver_address: {
hash: '0xD578780f1dA7404d9CC0eEbC9D684c140CC4b638',
},
resolved_with_wildcard: true,
stored_offchain: true,
wrapped_owner: {
hash: '0xD4416b13d2b3a9aBae7AcD5D6C2BbDBE25686401',
},
};
export const ensDomainB: bens.DetailedDomain = {
......@@ -81,6 +89,8 @@ export const ensDomainB: bens.DetailedDomain = {
expiry_date: undefined,
other_addresses: {},
protocol: undefined,
resolved_with_wildcard: false,
stored_offchain: false,
};
export const ensDomainC: bens.DetailedDomain = {
......@@ -101,6 +111,8 @@ export const ensDomainC: bens.DetailedDomain = {
expiry_date: '2022-11-01T13:10:36.000Z',
other_addresses: {},
protocol: undefined,
resolved_with_wildcard: false,
stored_offchain: false,
};
export const ensDomainD: bens.DetailedDomain = {
......@@ -119,4 +131,6 @@ export const ensDomainD: bens.DetailedDomain = {
expiry_date: '2027-09-23T13:10:36.000Z',
other_addresses: {},
protocol: undefined,
resolved_with_wildcard: false,
stored_offchain: false,
};
/* eslint-disable max-len */
import type { Transaction } from 'types/api/transaction';
import * as addressMock from 'mocks/address/address';
import { publicTag, privateTag, watchlistName } from 'mocks/address/tag';
import * as tokenTransferMock from 'mocks/tokens/tokenTransfer';
import * as decodedInputDataMock from 'mocks/txs/decodedInputData';
......@@ -47,7 +48,7 @@ export const base: Transaction = {
status: 'ok',
timestamp: '2022-10-10T14:34:30.000000Z',
to: {
hash: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859',
hash: addressMock.hash,
implementations: null,
is_contract: false,
is_verified: true,
......@@ -110,7 +111,7 @@ export const withTokenTransfer: Transaction = {
...base,
hash: '0x62d597ebcf3e8d60096dd0363bc2f0f5e2df27ba1dacd696c51aa7c9409f3196',
to: {
hash: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859',
hash: addressMock.hash,
implementations: null,
is_contract: true,
is_verified: true,
......@@ -166,7 +167,7 @@ export const withRawRevertReason: Transaction = {
raw: '4f6e6c79206368616972706572736f6e2063616e206769766520726967687420746f20766f74652e',
},
to: {
hash: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859',
hash: addressMock.hash,
implementations: null,
is_verified: true,
is_contract: true,
......@@ -344,7 +345,7 @@ export const base2 = {
hash: '0x02d597ebcf3e8d60096dd0363bc2f0f5e2df27ba1dacd696c51aa7c9409f3193',
from: {
...base.from,
hash: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859',
hash: addressMock.hash,
},
};
......@@ -353,7 +354,7 @@ export const base3 = {
hash: '0x12d597ebcf3e8d60096dd0363bc2f0f5e2df27ba1dacd696c51aa7c9409f3193',
from: {
...base.from,
hash: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859',
hash: addressMock.hash,
},
};
......@@ -375,3 +376,18 @@ export const withBlob = {
tx_types: [ 'blob_transaction' as const ],
type: 3,
};
export const withRecipientName = {
...base,
to: addressMock.withName,
};
export const withRecipientEns = {
...base,
to: addressMock.withEns,
};
export const withRecipientNameTag = {
...withRecipientEns,
to: addressMock.withNameTag,
};
import type { TxInterpretationResponse } from 'types/api/txInterpretation';
import { hash } from 'mocks/address/address';
export const txInterpretation: TxInterpretationResponse = {
data: {
summaries: [ {
......@@ -25,7 +27,7 @@ export const txInterpretation: TxInterpretationResponse = {
to_address: {
type: 'address',
value: {
hash: '0x48c04ed5691981C42154C6167398f95e8f38a7fF',
hash: hash,
implementations: null,
is_contract: false,
is_verified: false,
......
......@@ -12,6 +12,7 @@ export function walletConnect(): CspDev.DirectiveDescriptor {
return {
'connect-src': [
'*.web3modal.com',
'*.web3modal.org',
'*.walletconnect.com',
'wss://relay.walletconnect.com',
'wss://www.walletlink.org',
......
......@@ -21,6 +21,7 @@ declare module "nextjs-routes" {
| StaticRoute<"/api/log">
| StaticRoute<"/api/media-type">
| StaticRoute<"/api/metrics">
| StaticRoute<"/api/monitoring/invalid-api-schema">
| StaticRoute<"/api/proxy">
| StaticRoute<"/api/sprite">
| StaticRoute<"/api-docs">
......
......@@ -36,7 +36,7 @@
"monitoring:grafana:local": "docker run -d -p 4000:3000 --name=blockscout_grafana --user $(id -u) --volume $(pwd)/grafana:/var/lib/grafana grafana/grafana-enterprise"
},
"dependencies": {
"@blockscout/bens-types": "1.3.4",
"@blockscout/bens-types": "1.4.1",
"@blockscout/stats-types": "1.6.0",
"@blockscout/visualizer-types": "0.2.0",
"@chakra-ui/react": "2.7.1",
......@@ -108,6 +108,7 @@
"react-scroll": "^1.8.7",
"swagger-ui-react": "^5.9.0",
"use-font-face-observer": "^1.2.1",
"valibot": "0.38.0",
"viem": "2.10.9",
"wagmi": "2.9.2",
"xss": "^1.0.14"
......
......@@ -6,7 +6,7 @@ import React from 'react';
import logRequestFromBot from 'nextjs/utils/logRequestFromBot';
import * as serverTiming from 'nextjs/utils/serverTiming';
import theme from 'theme';
import theme from 'theme/theme';
import * as svgSprite from 'ui/shared/IconSvg';
class MyDocument extends Document {
......
import type { NextApiRequest, NextApiResponse } from 'next';
import * as v from 'valibot';
import metrics from 'lib/monitoring/metrics';
const PayloadSchema = v.object({
resource: v.string(),
url: v.string(),
});
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
const payload = JSON.parse(req.body);
metrics?.invalidApiSchema.inc(v.parse(PayloadSchema, payload));
} catch (error) {
}
res.status(200).json({ status: 'ok' });
}
......@@ -12,7 +12,7 @@ import config from 'configs/app';
import { AppContextProvider } from 'lib/contexts/app';
import { SocketProvider } from 'lib/socket/context';
import currentChain from 'lib/web3/currentChain';
import theme from 'theme';
import theme from 'theme/theme';
import { port as socketPort } from './utils/socket';
......
......@@ -37,6 +37,7 @@ export const ENVS_MAP: Record<string, Array<[string, string]>> = {
zkSyncRollup: [
[ 'NEXT_PUBLIC_ROLLUP_TYPE', 'zkSync' ],
[ 'NEXT_PUBLIC_ROLLUP_L1_BASE_URL', 'https://localhost:3101' ],
[ 'NEXT_PUBLIC_VIEWS_CONTRACT_EXTRA_VERIFICATION_METHODS', 'none' ],
],
bridgedTokens: [
[ 'NEXT_PUBLIC_BRIDGED_TOKENS_CHAINS', '[{"id":"1","title":"Ethereum","short_title":"ETH","base_url":"https://eth.blockscout.com/token/"},{"id":"56","title":"Binance Smart Chain","short_title":"BSC","base_url":"https://bscscan.com/token/"},{"id":"99","title":"POA","short_title":"POA","base_url":"https://blockscout.com/poa/core/token/"}]' ],
......
......@@ -22,6 +22,8 @@ export const ENS_DOMAIN: bens.DetailedDomain = {
ETH: ADDRESS_HASH,
},
protocol: undefined,
resolved_with_wildcard: false,
stored_offchain: false,
};
export const ENS_DOMAIN_EVENT: bens.DomainEvent = {
......
......@@ -23,6 +23,7 @@ export const ARBITRUM_L2_TXN_BATCHES_ITEM: ArbitrumL2TxnBatchesItem = {
hash: TX_HASH,
status: 'finalized',
},
batch_data_container: 'in_blob4844',
};
export const ARBITRUM_L2_TXN_BATCH: ArbitrumL2TxnBatch = {
......@@ -31,4 +32,7 @@ export const ARBITRUM_L2_TXN_BATCH: ArbitrumL2TxnBatch = {
before_acc: '0x2ed7c4985eb778d76ec400a43805e7feecc8c2afcdb492dbe5caf227de6d37bc',
start_block: 1245209,
end_block: 1245490,
data_availability: {
batch_data_container: 'in_blob4844',
},
};
import type { SmartContract, SolidityscanReport } from 'types/api/contract';
import type { SmartContract } from 'types/api/contract';
import type { VerifiedContract, VerifiedContractsCounters } from 'types/api/contracts';
import type { SolidityScanReport } from 'lib/solidityScan/schema';
import { ADDRESS_PARAMS } from './addressParams';
export const CONTRACT_CODE_UNVERIFIED = {
......@@ -78,7 +80,7 @@ export const VERIFIED_CONTRACTS_COUNTERS: VerifiedContractsCounters = {
new_verified_smart_contracts_24h: '1234',
};
export const SOLIDITYSCAN_REPORT: SolidityscanReport = {
export const SOLIDITY_SCAN_REPORT: SolidityScanReport = {
scan_report: {
contractname: 'BullRunners',
scan_status: 'scan_done',
......@@ -91,11 +93,7 @@ export const SOLIDITYSCAN_REPORT: SolidityscanReport = {
low: 2,
medium: 0,
},
lines_analyzed_count: 18,
scan_time_taken: 1,
score: '3.61',
score_v2: '72.22',
threat_score: '94.74',
},
scanner_reference_url: 'https://solidityscan.com/quickscan/0xc1EF7811FF2ebFB74F80ed7423f2AdAA37454be2/blockscout/eth-goerli?ref=blockscout',
},
......
/node_modules
/dist
\ No newline at end of file
This diff is collapsed.
{
"name": "@blockscout/chakra-theme",
"version": "1.32.0",
"main": "./dist/index.js",
"license": "MIT",
"scripts": {
"build": "yarn webpack-cli -c ./webpack.config.js"
},
"devDependencies": {
"ts-loader": "^9.4.4",
"webpack": "^5.88.2",
"webpack-cli": "^5.1.4",
"tsconfig-paths-webpack-plugin": "^4.1.0"
},
"files": [
"/dist"
]
}
{
"extends": "../tsconfig.json",
"compilerOptions": {
"noEmit": false,
"target": "es2016",
"module": "CommonJS",
"moduleResolution": "node",
"paths": {
"nextjs-routes": ["./nextjs/nextjs-routes.d.ts"],
}
},
"include": [
"../types/**/*.ts",
"../configs/app/**/*.ts",
"../global.d.ts",
"./theme.ts"
],
"tsc-alias": {
"verbose": true,
"resolveFullPaths": true,
}
}
\ No newline at end of file
const path = require('path');
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
const config = {
mode: 'production',
entry: path.resolve(__dirname) + '/theme.ts',
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
],
},
resolve: {
extensions: [ '.tsx', '.ts', '.js' ],
plugins: [ new TsconfigPathsPlugin({ configFile: './tsconfig.json' }) ],
},
output: {
filename: 'index.js',
path: path.resolve(__dirname) + '/dist',
library: {
type: 'commonjs',
},
},
optimization: {
minimize: false,
},
};
module.exports = config;
This diff is collapsed.
......@@ -33,11 +33,14 @@ type ArbitrumL2BatchCommitmentTx = {
timestamp: string;
}
type BatchDataContainer = 'in_blob4844' | 'in_calldata' | 'in_anytrust' | 'in_celestia' | null;
export type ArbitrumL2TxnBatchesItem = {
blocks_count: number;
commitment_transaction: ArbitrumL2BatchCommitmentTx;
number: number;
transactions_count: number;
batch_data_container: BatchDataContainer;
}
export type ArbitrumL2TxnBatchesResponse = {
......@@ -48,6 +51,22 @@ export type ArbitrumL2TxnBatchesResponse = {
} | null;
}
export type ArbitrumL2TxnBatchDAAnytrust = {
batch_data_container: 'in_anytrust';
bls_signature: string;
data_hash: string;
timeout: string;
signers: Array<{
key: string;
trusted: boolean;
proof?: string;
}>;
}
export type ArbitrumL2TxnBatchDataAvailability = ArbitrumL2TxnBatchDAAnytrust | {
batch_data_container: Exclude<BatchDataContainer, 'in_anytrust'>;
}
export type ArbitrumL2TxnBatch = {
after_acc: string;
before_acc: string;
......@@ -56,6 +75,7 @@ export type ArbitrumL2TxnBatch = {
start_block: number;
number: number;
transactions_count: number;
data_availability: ArbitrumL2TxnBatchDataAvailability;
}
export type ArbitrumL2BatchTxs = {
......@@ -84,3 +104,5 @@ export const ARBITRUM_L2_TX_BATCH_STATUSES = [
];
export type ArbitrumBatchStatus = typeof ARBITRUM_L2_TX_BATCH_STATUSES[number];
export type NewArbitrumBatchSocketResponse = { batch: ArbitrumL2TxnBatchesItem }
......@@ -27,7 +27,7 @@ export interface SmartContract {
compiler_version: string | null;
evm_version: string | null;
optimization_enabled: boolean | null;
optimization_runs: number | null;
optimization_runs: number | string | null;
name: string | null;
verified_at: string | null;
is_blueprint: boolean | null;
......@@ -57,6 +57,7 @@ export interface SmartContract {
language: string | null;
license_type: SmartContractLicenseType | null;
certified?: boolean;
zk_compiler_version?: string;
}
export type SmartContractDecodedConstructorArg = [
......@@ -86,6 +87,8 @@ export interface SmartContractVerificationConfigRaw {
vyper_evm_versions: Array<string>;
is_rust_verifier_microservice_enabled: boolean;
license_types: Record<SmartContractLicenseType, number>;
zk_compiler_versions?: Array<string>;
zk_optimization_modes?: Array<string>;
}
export type SmartContractVerificationResponse = {
......@@ -104,29 +107,6 @@ export interface SmartContractVerificationError {
name?: Array<string>;
}
export type SolidityscanReport = {
scan_report: {
contractname: string;
scan_status: string;
scan_summary: {
issue_severity_distribution: {
critical: number;
gas: number;
high: number;
informational: number;
low: number;
medium: number;
};
lines_analyzed_count: number;
scan_time_taken: number;
score: string;
score_v2: string;
threat_score: string;
};
scanner_reference_url: string;
};
}
type SmartContractSecurityAudit = {
audit_company_name: string;
audit_publish_date: string;
......
......@@ -13,6 +13,7 @@ export interface VerifiedContract {
verified_at: string;
market_cap: string | null;
license_type: SmartContractLicenseType | null;
zk_compiler_version?: string;
}
export interface VerifiedContractsResponse {
......
......@@ -111,8 +111,14 @@ type ArbitrumTransactionData = {
network_fee: string;
poster_fee: string;
status: ArbitrumBatchStatus;
message_related_info: {
associated_l1_transaction: string | null;
message_status: ArbitrumMessageStatus;
};
}
export type ArbitrumMessageStatus = 'Relayed' | 'Syncing with base layer' | 'Waiting for confirmation' | 'Ready for relay' | 'Settlement pending';
export const ZKEVM_L2_TX_STATUSES = [ 'Confirmed by Sequencer', 'L1 Confirmed' ];
export interface TransactionsStats {
......
import type { SolidityscanReport } from 'types/api/contract';
import type { SolidityScanReport, SolidityScanReportSeverityDistribution } from 'lib/solidityScan/schema';
export type MarketplaceAppPreview = {
id: string;
......@@ -54,12 +54,12 @@ export type MarketplaceAppSecurityReport = {
solidityScanContractsNumber: number;
securityScore: number;
totalIssues?: number;
issueSeverityDistribution: SolidityscanReport['scan_report']['scan_summary']['issue_severity_distribution'];
issueSeverityDistribution: SolidityScanReportSeverityDistribution;
};
contractsData: Array<{
address: string;
isVerified: boolean;
solidityScanReport?: SolidityscanReport['scan_report'] | null;
solidityScanReport?: SolidityScanReport['scan_report'] | null;
}>;
}
......
......@@ -24,4 +24,6 @@ export interface NetworkExplorer {
};
}
export type NetworkVerificationType = 'mining' | 'validation';
export type NetworkVerificationTypeEnvs = 'mining' | 'validation';
export type NetworkVerificationTypeComputed = 'posting' | 'sequencing';
export type NetworkVerificationType = NetworkVerificationTypeEnvs | NetworkVerificationTypeComputed;
......@@ -18,12 +18,14 @@ import ActionBar, { ACTION_BAR_HEIGHT_DESKTOP } from 'ui/shared/ActionBar';
import DataListDisplay from 'ui/shared/DataListDisplay';
import Pagination from 'ui/shared/pagination/Pagination';
import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
import SocketAlert from 'ui/shared/SocketAlert';
import * as SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice';
import { default as Thead } from 'ui/shared/TheadSticky';
import AddressBlocksValidatedListItem from './blocksValidated/AddressBlocksValidatedListItem';
import AddressBlocksValidatedTableItem from './blocksValidated/AddressBlocksValidatedTableItem';
const OVERLOAD_COUNT = 75;
interface Props {
scrollRef?: React.RefObject<HTMLDivElement>;
shouldRender?: boolean;
......@@ -31,7 +33,9 @@ interface Props {
}
const AddressBlocksValidated = ({ scrollRef, shouldRender = true, isQueryEnabled = true }: Props) => {
const [ socketAlert, setSocketAlert ] = React.useState(false);
const [ socketAlert, setSocketAlert ] = React.useState('');
const [ newItemsCount, setNewItemsCount ] = React.useState(0);
const queryClient = useQueryClient();
const router = useRouter();
const isMounted = useIsMounted();
......@@ -57,11 +61,11 @@ const AddressBlocksValidated = ({ scrollRef, shouldRender = true, isQueryEnabled
});
const handleSocketError = React.useCallback(() => {
setSocketAlert(true);
setSocketAlert('An error has occurred while fetching new blocks. Please refresh the page to load new blocks.');
}, []);
const handleNewSocketMessage: SocketMessage.NewBlock['handler'] = React.useCallback((payload) => {
setSocketAlert(false);
setSocketAlert('');
queryClient.setQueryData(
getResourceKey('address_blocks_validated', { pathParams: { hash: addressHash } }),
......@@ -70,6 +74,11 @@ const AddressBlocksValidated = ({ scrollRef, shouldRender = true, isQueryEnabled
return;
}
if (prevData.items.length >= OVERLOAD_COUNT) {
setNewItemsCount(prev => prev + 1);
return prevData;
}
return {
...prevData,
items: [ payload.block, ...prevData.items ],
......@@ -95,20 +104,26 @@ const AddressBlocksValidated = ({ scrollRef, shouldRender = true, isQueryEnabled
const content = query.data?.items ? (
<>
{ socketAlert && <SocketAlert mb={ 6 }/> }
<Hide below="lg" ssr={ false }>
<Table variant="simple" size="sm">
<Table variant="simple" size="sm" style={{ tableLayout: 'auto' }}>
<Thead top={ query.pagination.isVisible ? ACTION_BAR_HEIGHT_DESKTOP : 0 }>
<Tr>
<Th width="17%">Block</Th>
<Th width="17%">Age</Th>
<Th width="16%">Txn</Th>
<Th width="25%">Gas used</Th>
{ !config.UI.views.block.hiddenFields?.total_reward &&
<Th width="25%" isNumeric>Reward { currencyUnits.ether }</Th> }
<Th>Block</Th>
<Th>Age</Th>
<Th>Txn</Th>
<Th>Gas used</Th>
{ !config.UI.views.block.hiddenFields?.total_reward && !config.features.rollup.isEnabled &&
<Th isNumeric>Reward { currencyUnits.ether }</Th> }
</Tr>
</Thead>
<Tbody>
<SocketNewItemsNotice.Desktop
url={ window.location.href }
num={ newItemsCount }
alert={ socketAlert }
type="block"
isLoading={ query.isPlaceholderData }
/>
{ query.data.items.map((item, index) => (
<AddressBlocksValidatedTableItem
key={ item.height + (query.isPlaceholderData ? String(index) : '') }
......@@ -121,6 +136,15 @@ const AddressBlocksValidated = ({ scrollRef, shouldRender = true, isQueryEnabled
</Table>
</Hide>
<Show below="lg" ssr={ false }>
{ query.pagination.page === 1 && (
<SocketNewItemsNotice.Mobile
url={ window.location.href }
num={ newItemsCount }
alert={ socketAlert }
type="block"
isLoading={ query.isPlaceholderData }
/>
) }
{ query.data.items.map((item, index) => (
<AddressBlocksValidatedListItem
key={ item.height + (query.isPlaceholderData ? String(index) : '') }
......
......@@ -9,7 +9,7 @@ const addressHash = 'hash';
test('average report +@dark-mode +@mobile', async({ render, mockApiResponse, page }) => {
await mockApiResponse(
'contract_solidityscan_report',
'contract_solidity_scan_report',
solidityscanReportMock.solidityscanReportAverage,
{ pathParams: { hash: addressHash } },
);
......@@ -23,7 +23,7 @@ test('average report +@dark-mode +@mobile', async({ render, mockApiResponse, pag
test('great report', async({ render, mockApiResponse, page }) => {
await mockApiResponse(
'contract_solidityscan_report',
'contract_solidity_scan_report',
solidityscanReportMock.solidityscanReportGreat,
{ pathParams: { hash: addressHash } },
);
......@@ -41,7 +41,7 @@ test('great report', async({ render, mockApiResponse, page }) => {
test('low report', async({ render, mockApiResponse, page }) => {
await mockApiResponse(
'contract_solidityscan_report',
'contract_solidity_scan_report',
solidityscanReportMock.solidityscanReportLow,
{ pathParams: { hash: addressHash } },
);
......
......@@ -5,8 +5,7 @@ import React from 'react';
// Probably because of the gradient
// eslint-disable-next-line no-restricted-imports
import solidityScanIcon from 'icons/brands/solidity_scan.svg';
import useApiQuery from 'lib/api/useApiQuery';
import { SOLIDITYSCAN_REPORT } from 'stubs/contract';
import useFetchReport from 'lib/solidityScan/useFetchReport';
import Popover from 'ui/shared/chakra/Popover';
import LinkExternal from 'ui/shared/links/LinkExternal';
import SolidityscanReportButton from 'ui/shared/solidityscanReport/SolidityscanReportButton';
......@@ -20,22 +19,19 @@ interface Props {
const SolidityscanReport = ({ hash }: Props) => {
const { isOpen, onToggle, onClose } = useDisclosure();
const { data, isPlaceholderData, isError } = useApiQuery('contract_solidityscan_report', {
pathParams: { hash },
queryOptions: {
enabled: Boolean(hash),
placeholderData: SOLIDITYSCAN_REPORT,
retry: 0,
},
});
const { data, isPlaceholderData, isError } = useFetchReport({ hash });
const score = Number(data?.scan_report.scan_summary.score_v2);
if (isError || !data) {
return null;
}
const score = Number(data.scan_report.scan_summary.score_v2);
if (isError || !score) {
if (!score) {
return null;
}
const vulnerabilities = data?.scan_report.scan_summary.issue_severity_distribution;
const vulnerabilities = data.scan_report.scan_summary.issue_severity_distribution;
const vulnerabilitiesCounts = vulnerabilities ? Object.values(vulnerabilities) : [];
const vulnerabilitiesCount = vulnerabilitiesCounts.reduce((acc, val) => acc + val, 0);
......@@ -63,7 +59,7 @@ const SolidityscanReport = ({ hash }: Props) => {
<SolidityscanReportDetails vulnerabilities={ vulnerabilities } vulnerabilitiesCount={ vulnerabilitiesCount }/>
</Box>
) }
<LinkExternal href={ data?.scan_report.scanner_reference_url }>View full report</LinkExternal>
<LinkExternal href={ data.scan_report.scanner_reference_url }>View full report</LinkExternal>
</PopoverBody>
</PopoverContent>
</Popover>
......
......@@ -41,7 +41,7 @@ const AddressAccountHistoryListItem = (props: Props) => {
</Text>
</Flex>
<TimeAgoWithTooltip
timestamp={ (props.tx.rawTransactionData.timestamp * 1000).toString() }
timestamp={ props.tx.rawTransactionData.timestamp * 1000 }
color="text_secondary"
borderRadius="sm"
fontWeight={ 500 }
......
......@@ -26,7 +26,7 @@ const AddressAccountHistoryTableItem = (props: Props) => {
<Tr>
<Td px={ 3 } py="18px" fontSize="sm" >
<TimeAgoWithTooltip
timestamp={ (props.tx.rawTransactionData.timestamp * 1000).toString() }
timestamp={ props.tx.rawTransactionData.timestamp * 1000 }
isLoading={ props.isPlaceholderData }
color="text_secondary"
borderRadius="sm"
......
......@@ -52,7 +52,7 @@ const AddressBlocksValidatedListItem = (props: Props) => {
isLoading={ props.isLoading }
/>
</Flex>
{ !config.UI.views.block.hiddenFields?.total_reward && (
{ !config.UI.views.block.hiddenFields?.total_reward && !config.features.rollup.isEnabled && (
<Flex columnGap={ 2 } w="100%">
<Skeleton isLoaded={ !props.isLoading } fontWeight={ 500 } flexShrink={ 0 }>Reward { currencyUnits.ether }</Skeleton>
<Skeleton isLoaded={ !props.isLoading } color="text_secondary">{ totalReward.toFixed() }</Skeleton>
......
......@@ -56,7 +56,7 @@ const AddressBlocksValidatedTableItem = (props: Props) => {
/>
</Flex>
</Td>
{ !config.UI.views.block.hiddenFields?.total_reward && (
{ !config.UI.views.block.hiddenFields?.total_reward && !config.features.rollup.isEnabled && (
<Td isNumeric display="flex" justifyContent="end">
<Skeleton isLoaded={ !props.isLoading } display="inline-block">
<span>{ totalReward.toFixed() }</span>
......
......@@ -126,6 +126,14 @@ test('non verified', async({ render, mockApiResponse }) => {
await expect(component).toHaveScreenshot();
});
test('zkSync contract', async({ render, mockApiResponse, page, mockEnvs }) => {
await mockEnvs(ENVS_MAP.zkSyncRollup);
await mockApiResponse('contract', contractMock.zkSync, { pathParams: { hash: addressMock.contract.hash } });
await render(<ContractCode/>, { hooksConfig }, { withSocket: true });
await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 1200, height: 300 } });
});
test.describe('with audits feature', () => {
test.beforeEach(async({ mockEnvs }) => {
......
......@@ -60,6 +60,8 @@ const InfoItem = chakra(({ label, content, hint, className, isLoading }: InfoIte
</GridItem>
));
const rollupFeature = config.features.rollup;
const ContractCode = ({ addressHash, contractQuery, channel }: Props) => {
const [ isChangedBytecodeSocket, setIsChangedBytecodeSocket ] = React.useState<boolean>();
......@@ -266,6 +268,7 @@ const ContractCode = ({ addressHash, contractQuery, channel }: Props) => {
<Grid templateColumns={{ base: '1fr', lg: '1fr 1fr' }} rowGap={ 4 } columnGap={ 6 } mb={ 8 }>
{ data.name && <InfoItem label="Contract name" content={ contractNameWithCertifiedIcon } isLoading={ isPlaceholderData }/> }
{ data.compiler_version && <InfoItem label="Compiler version" content={ data.compiler_version } isLoading={ isPlaceholderData }/> }
{ data.zk_compiler_version && <InfoItem label="ZK compiler version" content={ data.zk_compiler_version } isLoading={ isPlaceholderData }/> }
{ data.evm_version && <InfoItem label="EVM version" content={ data.evm_version } textTransform="capitalize" isLoading={ isPlaceholderData }/> }
{ licenseLink && (
<InfoItem
......@@ -277,8 +280,13 @@ const ContractCode = ({ addressHash, contractQuery, channel }: Props) => {
) }
{ typeof data.optimization_enabled === 'boolean' &&
<InfoItem label="Optimization enabled" content={ data.optimization_enabled ? 'true' : 'false' } isLoading={ isPlaceholderData }/> }
{ data.optimization_runs !== null &&
<InfoItem label="Optimization runs" content={ String(data.optimization_runs) } isLoading={ isPlaceholderData }/> }
{ data.optimization_runs !== null && (
<InfoItem
label={ rollupFeature.isEnabled && rollupFeature.type === 'zkSync' ? 'Optimization mode' : 'Optimization runs' }
content={ String(data.optimization_runs) }
isLoading={ isPlaceholderData }
/>
) }
{ data.verified_at &&
<InfoItem label="Verified at" content={ dayjs(data.verified_at).format('llll') } wordBreak="break-word" isLoading={ isPlaceholderData }/> }
{ data.file_path && <InfoItem label="Contract file path" content={ data.file_path } wordBreak="break-word" isLoading={ isPlaceholderData }/> }
......
......@@ -31,7 +31,7 @@ import PopoverTriggerTooltip from 'ui/shared/PopoverTriggerTooltip';
interface Props {
query: UseQueryResult<bens.LookupAddressResponse, ResourceError<unknown>>;
addressHash: string;
mainDomainName: string | null;
mainDomainName: string | null | undefined;
}
const DomainsGrid = ({ data }: { data: Array<bens.Domain> }) => {
......@@ -64,9 +64,9 @@ const AddressEnsDomains = ({ query, addressHash, mainDomainName }: Props) => {
return null;
}
const mainDomain = data.items.find((domain) => domain.name === mainDomainName);
const mainDomain = data.items.find((domain) => mainDomainName && domain.name === mainDomainName);
const ownedDomains = data.items.filter((domain) => {
if (domain.name === mainDomainName) {
if (mainDomainName && domain.name === mainDomainName) {
return false;
}
......
......@@ -10,6 +10,7 @@ import { route } from 'nextjs-routes';
import capitalizeFirstLetter from 'lib/capitalizeFirstLetter';
import dayjs from 'lib/date/dayjs';
import useIsMobile from 'lib/hooks/useIsMobile';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import IconSvg from 'ui/shared/IconSvg';
import LinkInternal from 'ui/shared/links/LinkInternal';
import { default as Thead } from 'ui/shared/TheadSticky';
......@@ -29,7 +30,7 @@ type Props = {
toggleSorting: (key: AddressMudRecordsSorting['sort']) => void;
setFilters: React.Dispatch<React.SetStateAction<AddressMudRecordsFilter>>;
filters: AddressMudRecordsFilter;
toggleTableHasHorisontalScroll: () => void;
toggleTableHasHorizontalScroll: () => void;
scrollRef?: React.RefObject<HTMLDivElement>;
hash: string;
}
......@@ -41,7 +42,7 @@ const AddressMudRecordsTable = ({
toggleSorting,
filters,
setFilters,
toggleTableHasHorisontalScroll,
toggleTableHasHorizontalScroll,
scrollRef,
hash,
}: Props) => {
......@@ -59,8 +60,8 @@ const AddressMudRecordsTable = ({
const toggleIsOpen = React.useCallback(() => {
isOpened && tableRef.current?.scroll({ left: 0 });
setIsOpened.toggle();
toggleTableHasHorisontalScroll();
}, [ setIsOpened, toggleTableHasHorisontalScroll, isOpened ]);
toggleTableHasHorizontalScroll();
}, [ setIsOpened, toggleTableHasHorizontalScroll, isOpened ]);
const onRecordClick = React.useCallback((e: React.MouseEvent) => {
if (e.metaKey || e.ctrlKey) {
......@@ -109,18 +110,19 @@ const AddressMudRecordsTable = ({
const colW = isMobile ? COL_MIN_WIDTH_MOBILE : COL_MIN_WIDTH;
const keys = (isOpened || !hasCut) ? data.schema.key_names : data.schema.key_names.slice(0, colsCutCount);
const values = (isOpened || !hasCut) ? data.schema.value_names : data.schema.value_names.slice(0, colsCutCount - data.schema.key_names.length);
const colsCount = (isOpened || !hasCut) ? totalColsCut : colsCutCount;
const tdStyles: StyleProps = {
wordBreak: 'break-word',
whiteSpace: 'normal',
minW: `${ colW }px`,
w: `${ colW }px`,
w: `${ 100 / colsCount }%`,
verticalAlign: 'top',
lineHeight: '20px',
};
const keys = (isOpened || !hasCut) ? data.schema.key_names : data.schema.key_names.slice(0, colsCutCount);
const values = (isOpened || !hasCut) ? data.schema.value_names : data.schema.value_names.slice(0, colsCutCount - data.schema.key_names.length);
const hasHorizontalScroll = isMobile || isOpened;
if (hasCut && !colsCutCount) {
......@@ -136,7 +138,7 @@ const AddressMudRecordsTable = ({
);
return (
// can't implement both horisontal table scroll and sticky header
// can't implement both horizontal table scroll and sticky header
<Box maxW="100%" overflowX={ hasHorizontalScroll ? 'scroll' : 'unset' } whiteSpace="nowrap" ref={ tableRef }>
<Table variant="simple" size="sm" style={{ tableLayout: 'fixed' }}>
<Thead top={ hasHorizontalScroll ? 0 : top } display={ hasHorizontalScroll ? 'table' : 'table-header-group' } w="100%">
......@@ -185,7 +187,7 @@ const AddressMudRecordsTable = ({
</Th>
)) }
{ hasCut && !isOpened && cutButton }
<Th { ...tdStyles }>Modified</Th>
<Th { ...tdStyles } w={ `${ colW }px` }>Modified</Th>
{ hasCut && isOpened && cutButton }
</Tr>
</Thead>
......@@ -204,12 +206,13 @@ const AddressMudRecordsTable = ({
{ getValueString(item.decoded[keyName]) }
</LinkInternal>
) : getValueString(item.decoded[keyName]) }
<CopyToClipboard text={ item.decoded[keyName] }/>
</Td>
)) }
{ values.map((valName) =>
<Td key={ valName } { ...tdStyles }>{ getValueString(item.decoded[valName]) }</Td>) }
{ hasCut && !isOpened && <Td width={ `${ CUT_COL_WIDTH }px ` }></Td> }
<Td { ...tdStyles } color="text_secondary">{ dayjs(item.timestamp).format('lll') }</Td>
<Td { ...tdStyles } color="text_secondary" w={ `${ colW }px` }>{ dayjs(item.timestamp).format('lll') }</Td>
{ hasCut && isOpened && <Td width={ `${ CUT_COL_WIDTH }px ` }></Td> }
</Tr>
)) }
......
......@@ -36,7 +36,7 @@ const AddressMudTable = ({ scrollRef, tableId, isQueryEnabled = true }: Props) =
React.useState<AddressMudRecordsSorting | undefined>(getSortParamsFromQuery<AddressMudRecordsSorting>(router.query, SORT_SEQUENCE));
const [ filters, setFilters ] = React.useState<AddressMudRecordsFilter>({});
const isMobile = useIsMobile();
const [ tableHasHorisontalScroll, setTableHasHorisontalScroll ] = useBoolean(isMobile);
const [ tableHasHorizontalScroll, setTableHasHorizontalScroll ] = useBoolean(isMobile);
const hash = getQueryParamString(router.query.hash);
......@@ -109,7 +109,7 @@ const AddressMudTable = ({ scrollRef, tableId, isQueryEnabled = true }: Props) =
) : null;
const actionBar = (!isMobile || hasActiveFilters || pagination.isVisible) && (
<ActionBar mt={ -6 } showShadow={ tableHasHorisontalScroll } justifyContent="space-between" alignItems={ hasActiveFilters ? 'start' : 'center' }>
<ActionBar mt={ -6 } showShadow={ tableHasHorizontalScroll } justifyContent="space-between" alignItems={ hasActiveFilters ? 'start' : 'center' }>
<Box>
{ !isMobile && breadcrumbs }
{ filtersTags }
......@@ -126,7 +126,7 @@ const AddressMudTable = ({ scrollRef, tableId, isQueryEnabled = true }: Props) =
toggleSorting={ toggleSorting }
setFilters={ setFilters }
filters={ filters }
toggleTableHasHorisontalScroll={ setTableHasHorisontalScroll.toggle }
toggleTableHasHorizontalScroll={ setTableHasHorizontalScroll.toggle }
scrollRef={ scrollRef }
hash={ hash }
/>
......
......@@ -43,7 +43,7 @@ const AddressMudTablesTableItem = ({ item, isLoading, scrollRef, hash }: Props)
return (
<>
<Tr borderStyle={ isOpened ? 'hidden' : 'unset' }>
<Tr borderBottomStyle={ isOpened ? 'hidden' : 'unset' }>
<Td verticalAlign="middle">
<Skeleton isLoaded={ !isLoading }>
<Link display="block">
......
......@@ -25,8 +25,7 @@ const AddressesTable = ({ items, totalSupply, pageStartIndex, top, isLoading }:
<Thead top={ top }>
<Tr>
<Th width="64px">Rank</Th>
<Th width={ hasPercentage ? '30%' : '40%' }>Address</Th>
<Th width="20%" pl={ 10 }>Public tag</Th>
<Th width={ hasPercentage ? '50%' : '60%' }>Address</Th>
<Th width={ hasPercentage ? '20%' : '25%' } isNumeric>{ `Balance ${ currencyUnits.ether }` }</Th>
{ hasPercentage && <Th width="15%" isNumeric>Percentage</Th> }
<Th width="15%" isNumeric>Txn count</Th>
......
import { Tr, Td, Text, Skeleton } from '@chakra-ui/react';
import { Tr, Td, Text, Skeleton, Flex } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import React from 'react';
......@@ -35,17 +35,17 @@ const AddressesTableItem = ({
</Skeleton>
</Td>
<Td>
<Flex alignItems="center" columnGap={ 2 }>
<AddressEntity
address={ item }
isLoading={ isLoading }
fontWeight={ 700 }
my="2px"
/>
</Td>
<Td pl={ 10 }>
{ item.public_tags && item.public_tags.length ? item.public_tags.map(tag => (
<Tag key={ tag.label } isLoading={ isLoading } isTruncated>{ tag.display_name }</Tag>
)) : null }
</Flex>
</Td>
<Td isNumeric>
<Skeleton isLoaded={ !isLoading } display="inline-block" maxW="100%">
......
......@@ -15,6 +15,7 @@ import getBlockReward from 'lib/block/getBlockReward';
import { GWEI, WEI, WEI_IN_GWEI, ZERO } from 'lib/consts';
import getArbitrumVerificationStepStatus from 'lib/getArbitrumVerificationStepStatus';
import { space } from 'lib/html-entities';
import getNetworkValidationActionText from 'lib/networks/getNetworkValidationActionText';
import getNetworkValidatorTitle from 'lib/networks/getNetworkValidatorTitle';
import getQueryParamString from 'lib/router/getQueryParamString';
import { currencyUnits } from 'lib/units';
......@@ -114,13 +115,7 @@ const BlockDetails = ({ query }: Props) => {
);
})();
const verificationTitle = (() => {
if (rollupFeature.isEnabled && rollupFeature.type === 'zkEvm') {
return 'Sequenced by';
}
return config.chain.verificationType === 'validation' ? 'Validated by' : 'Mined by';
})();
const verificationTitle = `${ capitalize(getNetworkValidationActionText()) } by`;
const txsNum = (() => {
const blockTxsNum = (
......
......@@ -2,6 +2,7 @@ import React from 'react';
import type { SmartContractVerificationConfig } from 'types/client/contract';
import { ENVS_MAP } from 'playwright/fixtures/mockEnvs';
import * as socketServer from 'playwright/fixtures/socketServer';
import { test, expect } from 'playwright/lib';
......@@ -215,3 +216,17 @@ test('solidity-foundry method', async({ render, page }) => {
await expect(component).toHaveScreenshot();
});
test('verification of zkSync contract', async({ render, mockEnvs }) => {
const zkSyncFormConfig: SmartContractVerificationConfig = {
...formConfig,
verification_options: [ 'standard-input' ],
zk_compiler_versions: [ 'v1.4.1', 'v1.4.0', 'v1.3.23', 'v1.3.22' ],
zk_optimization_modes: [ '0', '1', '2', '3', 's', 'z' ],
};
await mockEnvs(ENVS_MAP.zkSyncRollup);
const component = await render(<ContractVerificationForm config={ zkSyncFormConfig } hash={ hash }/>, { hooksConfig });
await expect(component).toHaveScreenshot();
});
......@@ -41,7 +41,7 @@ interface Props {
const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Props) => {
const formApi = useForm<FormFields>({
mode: 'onBlur',
defaultValues: methodFromQuery ? getDefaultValues(methodFromQuery, config, hash, null) : undefined,
defaultValues: getDefaultValues(methodFromQuery, config, hash, null),
});
const { control, handleSubmit, watch, formState, setError, reset, getFieldState } = formApi;
const submitPromiseResolver = React.useRef<(value: unknown) => void>();
......
......@@ -4,14 +4,15 @@ import React from 'react';
interface Props {
title: string;
children: React.ReactNode;
disableScroll?: boolean;
}
const ContractVerificationMethod = ({ title, children }: Props) => {
const ContractVerificationMethod = ({ title, children, disableScroll }: Props) => {
const ref = React.useRef<HTMLDivElement>(null);
React.useEffect(() => {
ref.current?.scrollIntoView({ behavior: 'smooth' });
}, []);
!disableScroll && ref.current?.scrollIntoView({ behavior: 'smooth' });
}, [ disableScroll ]);
return (
<section ref={ ref }>
......
......@@ -51,6 +51,7 @@ const ContractVerificationFieldMethod = ({ control, isDisabled, methods }: Props
isDisabled={ isDisabled }
isRequired
isAsync={ false }
isReadOnly={ options.length === 1 }
/>
);
}, [ isDisabled, isMobile, options ]);
......
import { Box, Link } from '@chakra-ui/react';
import { useQueryClient } from '@tanstack/react-query';
import React from 'react';
import type { ControllerRenderProps } from 'react-hook-form';
import { useFormContext, Controller } from 'react-hook-form';
import type { FormFields } from '../types';
import type { SmartContractVerificationConfig } from 'types/client/contract';
import { getResourceKey } from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile';
import FancySelect from 'ui/shared/FancySelect/FancySelect';
import IconSvg from 'ui/shared/IconSvg';
import ContractVerificationFormRow from '../ContractVerificationFormRow';
const OPTIONS_LIMIT = 50;
const ContractVerificationFieldZkCompiler = () => {
const { formState, control } = useFormContext<FormFields>();
const isMobile = useIsMobile();
const queryClient = useQueryClient();
const config = queryClient.getQueryData<SmartContractVerificationConfig>(getResourceKey('contract_verification_config'));
const options = React.useMemo(() => (
config?.zk_compiler_versions?.map((option) => ({ label: option, value: option })) || []
), [ config?.zk_compiler_versions ]);
const loadOptions = React.useCallback(async(inputValue: string) => {
return options
.filter(({ label }) => !inputValue || label.toLowerCase().includes(inputValue.toLowerCase()))
.slice(0, OPTIONS_LIMIT);
}, [ options ]);
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'zk_compiler'>}) => {
const error = 'zk_compiler' in formState.errors ? formState.errors.zk_compiler : undefined;
return (
<FancySelect
{ ...field }
loadOptions={ loadOptions }
defaultOptions
size={ isMobile ? 'md' : 'lg' }
placeholder="ZK compiler (enter version or use the dropdown)"
placeholderIcon={ <IconSvg name="search"/> }
isDisabled={ formState.isSubmitting }
error={ error }
isRequired
isAsync
/>
);
}, [ formState.errors, formState.isSubmitting, isMobile, loadOptions ]);
return (
<ContractVerificationFormRow>
<Controller
name="zk_compiler"
control={ control }
render={ renderControl }
rules={{ required: true }}
/>
<Box>
<Link isExternal href="https://docs.zksync.io/zk-stack/components/compiler/specification#glossary">zksolc</Link>
<span> compiler version.</span>
</Box>
</ContractVerificationFormRow>
);
};
export default React.memo(ContractVerificationFieldZkCompiler);
import { Flex, Select } from '@chakra-ui/react';
import React from 'react';
import type { ControllerRenderProps } from 'react-hook-form';
import { Controller, useFormContext } from 'react-hook-form';
import type { FormFields } from '../types';
import type { SmartContractVerificationConfig } from 'types/client/contract';
import CheckboxInput from 'ui/shared/CheckboxInput';
import ContractVerificationFormRow from '../ContractVerificationFormRow';
interface Props {
config: SmartContractVerificationConfig;
}
const ContractVerificationFieldZkOptimization = ({ config }: Props) => {
const [ isEnabled, setIsEnabled ] = React.useState(false);
const { formState, control } = useFormContext<FormFields>();
const error = 'optimization_mode' in formState.errors ? formState.errors.optimization_mode : undefined;
const handleCheckboxChange = React.useCallback(() => {
setIsEnabled(prev => !prev);
}, []);
const renderCheckboxControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'is_optimization_enabled'>}) => (
<Flex flexShrink={ 0 }>
<CheckboxInput<FormFields, 'is_optimization_enabled'>
text="Optimization enabled"
field={ field }
onChange={ handleCheckboxChange }
isDisabled={ formState.isSubmitting }
/>
</Flex>
), [ formState.isSubmitting, handleCheckboxChange ]);
const renderInputControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'optimization_mode'>}) => {
return (
<Select
size="xs"
{ ...field }
w="auto"
borderRadius="base"
isDisabled={ formState.isSubmitting }
placeholder="Optimization mode"
isInvalid={ Boolean(error) }
>
{ config.zk_optimization_modes?.map((value) => (
<option key={ value } value={ value }>
{ value }
</option>
)) }
</Select>
);
}, [ config.zk_optimization_modes, error, formState.isSubmitting ]);
return (
<ContractVerificationFormRow>
<Flex columnGap={ 5 } rowGap={ 2 } h={{ base: 'auto', lg: '32px' }} flexDir={{ base: 'column', lg: 'row' }}>
<Controller
name="is_optimization_enabled"
control={ control }
render={ renderCheckboxControl }
/>
{ isEnabled && (
<Controller
name="optimization_mode"
control={ control }
render={ renderInputControl }
/>
) }
</Flex>
</ContractVerificationFormRow>
);
};
export default React.memo(ContractVerificationFieldZkOptimization);
......@@ -2,25 +2,32 @@ import React from 'react';
import type { SmartContractVerificationConfig } from 'types/client/contract';
import config from 'configs/app';
import ContractVerificationMethod from '../ContractVerificationMethod';
import ContractVerificationFieldAutodetectArgs from '../fields/ContractVerificationFieldAutodetectArgs';
import ContractVerificationFieldCompiler from '../fields/ContractVerificationFieldCompiler';
import ContractVerificationFieldName from '../fields/ContractVerificationFieldName';
import ContractVerificationFieldSources from '../fields/ContractVerificationFieldSources';
import ContractVerificationFieldZkCompiler from '../fields/ContractVerificationFieldZkCompiler';
import ContractVerificationFieldZkOptimization from '../fields/ContractVerificationFieldZkOptimization';
const FILE_TYPES = [ '.json' as const ];
const rollupFeature = config.features.rollup;
const ContractVerificationStandardInput = ({ config }: { config: SmartContractVerificationConfig }) => {
return (
<ContractVerificationMethod title="Contract verification via Solidity (standard JSON input) ">
<ContractVerificationMethod title="Contract verification via Solidity (standard JSON input) " disableScroll={ config.verification_options.length === 1 }>
{ !config?.is_rust_verifier_microservice_enabled && <ContractVerificationFieldName/> }
<ContractVerificationFieldCompiler/>
{ rollupFeature.isEnabled && rollupFeature.type === 'zkSync' && <ContractVerificationFieldZkCompiler/> }
<ContractVerificationFieldSources
fileTypes={ FILE_TYPES }
title="Standard Input JSON"
hint="Upload the standard input JSON file created during contract compilation."
required
/>
{ rollupFeature.isEnabled && rollupFeature.type === 'zkSync' && <ContractVerificationFieldZkOptimization config={ config }/> }
{ !config?.is_rust_verifier_microservice_enabled && <ContractVerificationFieldAutodetectArgs/> }
</ContractVerificationMethod>
);
......
......@@ -44,6 +44,20 @@ export interface FormFieldsStandardInput {
license_type: LicenseOption | null;
}
export interface FormFieldsStandardInputZk {
address: string;
method: MethodOption;
name: string;
compiler: Option | null;
zk_compiler: Option | null;
sources: Array<File>;
autodetect_constructor_args: boolean;
constructor_args: string;
license_type: LicenseOption | null;
is_optimization_enabled: boolean;
optimization_mode: string | undefined;
}
export interface FormFieldsSourcify {
address: string;
method: MethodOption;
......@@ -93,5 +107,5 @@ export interface FormFieldsVyperStandardInput {
license_type: LicenseOption | null;
}
export type FormFields = FormFieldsFlattenSourceCode | FormFieldsStandardInput | FormFieldsSourcify |
export type FormFields = FormFieldsFlattenSourceCode | FormFieldsStandardInput | FormFieldsStandardInputZk | FormFieldsSourcify |
FormFieldsMultiPartFile | FormFieldsVyperContract | FormFieldsVyperMultiPartFile | FormFieldsVyperStandardInput;
......@@ -7,6 +7,7 @@ import type {
FormFieldsMultiPartFile,
FormFieldsSourcify,
FormFieldsStandardInput,
FormFieldsStandardInputZk,
FormFieldsVyperContract,
FormFieldsVyperMultiPartFile,
FormFieldsVyperStandardInput,
......@@ -155,11 +156,18 @@ export const DEFAULT_VALUES: Record<SmartContractVerificationMethod, FormFields>
};
export function getDefaultValues(
method: SmartContractVerificationMethod,
methodParam: SmartContractVerificationMethod | undefined,
config: SmartContractVerificationConfig,
hash: string | undefined,
licenseType: FormFields['license_type'],
) {
const singleMethod = config.verification_options.length === 1 ? config.verification_options[0] : undefined;
const method = singleMethod || methodParam;
if (!method) {
return;
}
const defaultValues: FormFields = { ...DEFAULT_VALUES[method], address: hash || '', license_type: licenseType };
if ('evm_version' in defaultValues) {
......@@ -179,6 +187,13 @@ export function getDefaultValues(
}
}
if (singleMethod) {
defaultValues.method = {
label: METHOD_LABELS[config.verification_options[0]],
value: config.verification_options[0],
};
}
return defaultValues;
}
......@@ -223,7 +238,7 @@ export function prepareRequestBody(data: FormFields): FetchParams['body'] {
}
case 'standard-input': {
const _data = data as FormFieldsStandardInput;
const _data = data as (FormFieldsStandardInput | FormFieldsStandardInputZk);
const body = new FormData();
_data.compiler && body.set('compiler_version', _data.compiler.value);
......@@ -233,6 +248,15 @@ export function prepareRequestBody(data: FormFields): FetchParams['body'] {
body.set('constructor_args', _data.constructor_args);
addFilesToFormData(body, _data.sources, 'files');
// zkSync fields
'zk_compiler' in _data && _data.zk_compiler && body.set('zk_compiler_version', _data.zk_compiler.value);
if ('is_optimization_enabled' in _data) {
body.set('is_optimization_enabled', String(Boolean(_data.is_optimization_enabled)));
if (_data.is_optimization_enabled && 'optimization_mode' in _data && _data.optimization_mode) {
body.set('optimization_runs', _data.optimization_mode);
}
}
return body;
}
......
......@@ -69,7 +69,7 @@ const LatestBlocks = () => {
let content;
if (isError) {
content = <Text>No data. Please reload page.</Text>;
content = <Text>No data. Please reload the page.</Text>;
}
if (data) {
......
......@@ -26,7 +26,7 @@ const LatestTransactions = () => {
const { num, socketAlert } = useNewTxsSocket();
if (isError) {
return <Text mt={ 4 }>No data. Please reload page.</Text>;
return <Text mt={ 4 }>No data. Please reload the page.</Text>;
}
if (data) {
......
......@@ -23,7 +23,7 @@ const LatestWatchlistTxs = () => {
});
if (isError) {
return <Text mt={ 4 }>No data. Please reload page.</Text>;
return <Text mt={ 4 }>No data. Please reload the page.</Text>;
}
if (!data?.length) {
......
......@@ -46,13 +46,21 @@ const Stats = () => {
},
});
if (isError || zkEvmLatestBatchQuery.isError || zkSyncLatestBatchQuery.isError) {
const arbitrumLatestBatchQuery = useApiQuery('homepage_arbitrum_latest_batch', {
queryOptions: {
placeholderData: 12345,
enabled: rollupFeature.isEnabled && rollupFeature.type === 'arbitrum',
},
});
if (isError || zkEvmLatestBatchQuery.isError || zkSyncLatestBatchQuery.isError || arbitrumLatestBatchQuery.isError) {
return null;
}
const isLoading = isPlaceholderData ||
(rollupFeature.isEnabled && rollupFeature.type === 'zkEvm' && zkEvmLatestBatchQuery.isPlaceholderData) ||
(rollupFeature.isEnabled && rollupFeature.type === 'zkSync' && zkSyncLatestBatchQuery.isPlaceholderData);
(rollupFeature.isEnabled && rollupFeature.type === 'zkSync' && zkSyncLatestBatchQuery.isPlaceholderData) ||
(rollupFeature.isEnabled && rollupFeature.type === 'arbitrum' && arbitrumLatestBatchQuery.isPlaceholderData);
const content = (() => {
if (!data) {
......@@ -72,22 +80,21 @@ const Stats = () => {
</GasInfoTooltip>
) : null;
const hasBatches = rollupFeature.isEnabled && (rollupFeature.type === 'zkEvm' || rollupFeature.type === 'zkSync' || rollupFeature.type === 'arbitrum');
const latestBatch =
(hasBatches && rollupFeature.type === 'zkEvm' ? zkEvmLatestBatchQuery.data : null) ||
(hasBatches && rollupFeature.type === 'zkSync' ? zkSyncLatestBatchQuery.data : null) ||
(hasBatches && rollupFeature.type === 'arbitrum' ? arbitrumLatestBatchQuery.data : null) || 0;
const items: Array<StatsWidgetProps> = [
rollupFeature.isEnabled && rollupFeature.type === 'zkEvm' && {
icon: 'txn_batches_slim' as const,
label: 'Latest batch',
value: (zkEvmLatestBatchQuery.data || 0).toLocaleString(),
href: { pathname: '/batches' as const },
isLoading,
},
rollupFeature.isEnabled && rollupFeature.type === 'zkSync' && {
hasBatches && {
icon: 'txn_batches_slim' as const,
label: 'Latest batch',
value: (zkSyncLatestBatchQuery.data || 0).toLocaleString(),
value: latestBatch.toLocaleString(),
href: { pathname: '/batches' as const },
isLoading,
},
!(rollupFeature.isEnabled && (rollupFeature.type === 'zkEvm' || rollupFeature.type === 'zkSync')) && {
!hasBatches && {
icon: 'block_slim' as const,
label: 'Total blocks',
value: Number(data.total_blocks).toLocaleString(),
......
......@@ -3,10 +3,13 @@ import React from 'react';
import config from 'configs/app';
import useHasAccount from 'lib/hooks/useHasAccount';
import LatestDeposits from 'ui/home/LatestDeposits';
import LatestOptimisticDeposits from 'ui/home/latestDeposits/LatestOptimisticDeposits';
import LatestTxs from 'ui/home/LatestTxs';
import LatestWatchlistTxs from 'ui/home/LatestWatchlistTxs';
import TabsWithScroll from 'ui/shared/Tabs/TabsWithScroll';
import LatestArbitrumDeposits from './latestDeposits/LatestArbitrumDeposits';
const rollupFeature = config.features.rollup;
const TAB_LIST_PROPS = {
......@@ -15,10 +18,13 @@ const TAB_LIST_PROPS = {
const TransactionsHome = () => {
const hasAccount = useHasAccount();
if ((rollupFeature.isEnabled && rollupFeature.type === 'optimistic') || hasAccount) {
if ((rollupFeature.isEnabled && (rollupFeature.type === 'optimistic' || rollupFeature.type === 'arbitrum')) || hasAccount) {
const tabs = [
{ id: 'txn', title: 'Latest txn', component: <LatestTxs/> },
rollupFeature.isEnabled && rollupFeature.type === 'optimistic' && { id: 'deposits', title: 'Deposits (L1→L2 txn)', component: <LatestDeposits/> },
rollupFeature.isEnabled && rollupFeature.type === 'optimistic' &&
{ id: 'deposits', title: 'Deposits (L1→L2 txn)', component: <LatestOptimisticDeposits/> },
rollupFeature.isEnabled && rollupFeature.type === 'arbitrum' &&
{ id: 'deposits', title: 'Deposits (L1→L2 txn)', component: <LatestArbitrumDeposits/> },
hasAccount && { id: 'watchlist', title: 'Watch list', component: <LatestWatchlistTxs/> },
].filter(Boolean);
return (
......
import React from 'react';
import { finalized, unfinalized } from 'mocks/arbitrum/txnBatches';
import { ENVS_MAP } from 'playwright/fixtures/mockEnvs';
import { test, expect } from 'playwright/lib';
import LatestArbitrumL2Batches from './LatestArbitrumL2Batches';
test('default view +@mobile +@dark-mode', async({ render, mockEnvs, mockApiResponse }) => {
await mockEnvs(ENVS_MAP.arbitrumRollup);
await mockApiResponse('homepage_arbitrum_l2_batches', { items: [ finalized, unfinalized ] });
const component = await render(<LatestArbitrumL2Batches/>);
await expect(component).toHaveScreenshot();
});
import { Box, Heading, Flex, Text, VStack } from '@chakra-ui/react';
import { useQueryClient } from '@tanstack/react-query';
import { AnimatePresence } from 'framer-motion';
import React from 'react';
import type { SocketMessage } from 'lib/socket/types';
import type { ArbitrumL2TxnBatchesItem } from 'types/api/arbitrumL2';
import { route } from 'nextjs-routes';
import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile';
import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
import { ARBITRUM_L2_TXN_BATCHES_ITEM } from 'stubs/arbitrumL2';
import LinkInternal from 'ui/shared/links/LinkInternal';
import LatestBatchItem from './LatestBatchItem';
const LatestArbitrumL2Batches = () => {
const isMobile = useIsMobile();
const batchesMaxCount = isMobile ? 2 : 5;
const queryClient = useQueryClient();
const { data, isPlaceholderData, isError } = useApiQuery('homepage_arbitrum_l2_batches', {
queryOptions: {
placeholderData: { items: Array(batchesMaxCount).fill(ARBITRUM_L2_TXN_BATCHES_ITEM) },
},
});
const handleNewBatchMessage: SocketMessage.NewArbitrumL2Batch['handler'] = React.useCallback((payload) => {
queryClient.setQueryData(getResourceKey('homepage_arbitrum_l2_batches'), (prevData: { items: Array<ArbitrumL2TxnBatchesItem> } | undefined) => {
const newItems = prevData?.items ? [ ...prevData.items ] : [];
if (newItems.some((batch => batch.number === payload.batch.number))) {
return { items: newItems };
}
return { items: [ payload.batch, ...newItems ].sort((b1, b2) => b2.number - b1.number).slice(0, batchesMaxCount) };
});
}, [ queryClient, batchesMaxCount ]);
const channel = useSocketChannel({
topic: 'arbitrum:new_batch',
isDisabled: isPlaceholderData || isError,
});
useSocketMessage({
channel,
event: 'new_arbitrum_batch',
handler: handleNewBatchMessage,
});
let content;
if (isError) {
content = <Text>No data. Please reload the page.</Text>;
}
if (data) {
const dataToShow = data.items.slice(0, batchesMaxCount);
content = (
<>
<VStack spacing={ 2 } mb={ 3 } overflow="hidden" alignItems="stretch">
<AnimatePresence initial={ false } >
{ dataToShow.map(((batch, index) => (
<LatestBatchItem
key={ batch.number + (isPlaceholderData ? String(index) : '') }
number={ batch.number }
timestamp={ batch.commitment_transaction.timestamp }
txCount={ batch.transactions_count }
isLoading={ isPlaceholderData }
/>
))) }
</AnimatePresence>
</VStack>
<Flex justifyContent="center">
<LinkInternal fontSize="sm" href={ route({ pathname: '/batches' }) }>View all batches</LinkInternal>
</Flex>
</>
);
}
return (
<Box width={{ base: '100%', lg: '280px' }} flexShrink={ 0 }>
<Heading as="h4" size="sm" mb={ 3 }>Latest batches</Heading>
{ content }
</Box>
);
};
export default LatestArbitrumL2Batches;
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment