Commit c7aa0f68 authored by tom's avatar tom

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

parents 089ef89e 9cf6ce9a
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
......
......@@ -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
......@@ -96,4 +87,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';
......
......@@ -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';
......@@ -580,15 +582,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',
},
......@@ -601,6 +609,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: {
......@@ -966,11 +977,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 :
......@@ -1029,8 +1043,6 @@ 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 :
......@@ -1097,6 +1109,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(() => {
......
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>;
}
/* 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,
......
/* 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',
......
......@@ -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 {
......
......@@ -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/"}]' ],
......
......@@ -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',
},
};
/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,32 @@ export interface SmartContractVerificationError {
name?: Array<string>;
}
// it's an external API proxy, we can't guarantee the responce types
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;
scan_report?: {
contractname?: string;
scan_status?: string;
scan_summary?: {
issue_severity_distribution?: SolidityscanReportSeverityDistribution;
lines_analyzed_count?: number;
scan_time_taken?: number;
score?: string;
score_v2?: string;
threat_score?: string;
};
scanner_reference_url: string;
scanner_reference_url?: string;
};
}
export type SolidityscanReportSeverityDistribution = {
critical?: number;
gas?: number;
high?: number;
informational?: number;
low?: number;
medium?: number;
};
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 {
......
import type { SolidityscanReport } from 'types/api/contract';
import type { SolidityscanReport, SolidityscanReportSeverityDistribution } from 'types/api/contract';
export type MarketplaceAppPreview = {
id: string;
......@@ -54,7 +54,7 @@ export type MarketplaceAppSecurityReport = {
solidityScanContractsNumber: number;
securityScore: number;
totalIssues?: number;
issueSeverityDistribution: SolidityscanReport['scan_report']['scan_summary']['issue_severity_distribution'];
issueSeverityDistribution: SolidityscanReportSeverityDistribution;
};
contractsData: Array<{
address: string;
......
......@@ -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) : '') }
......
......@@ -29,13 +29,17 @@ const SolidityscanReport = ({ hash }: Props) => {
},
});
const score = Number(data?.scan_report.scan_summary.score_v2);
if (isError || !data?.scan_report?.scan_summary) {
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);
......
......@@ -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 }/> }
......
......@@ -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>
<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 alignItems="center" columnGap={ 2 }>
<AddressEntity
address={ item }
isLoading={ isLoading }
fontWeight={ 700 }
my="2px"
/>
{ 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;
......@@ -6,21 +6,21 @@ import {
import { motion } from 'framer-motion';
import React from 'react';
import type { ZkEvmL2TxnBatchesItem } from 'types/api/zkEvmL2';
import { route } from 'nextjs-routes';
import BatchEntityL2 from 'ui/shared/entities/block/BatchEntityL2';
import LinkInternal from 'ui/shared/links/LinkInternal';
import ZkEvmL2TxnBatchStatus from 'ui/shared/statusTag/ZkEvmL2TxnBatchStatus';
import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip';
type Props = {
batch: ZkEvmL2TxnBatchesItem;
isLoading?: boolean;
number: number;
timestamp: string | null;
txCount: number;
status?: React.ReactNode;
isLoading: boolean;
}
const LatestZkevmL2BatchItem = ({ batch, isLoading }: Props) => {
const LatestBatchItem = ({ number, timestamp, txCount, status, isLoading }: Props) => {
return (
<Box
as={ motion.div }
......@@ -37,7 +37,7 @@ const LatestZkevmL2BatchItem = ({ batch, isLoading }: Props) => {
<Flex alignItems="center" overflow="hidden" w="100%" mb={ 3 }>
<BatchEntityL2
isLoading={ isLoading }
number={ batch.number }
number={ number }
tailLength={ 2 }
fontSize="xl"
lineHeight={ 7 }
......@@ -45,7 +45,7 @@ const LatestZkevmL2BatchItem = ({ batch, isLoading }: Props) => {
mr="auto"
/>
<TimeAgoWithTooltip
timestamp={ batch.timestamp }
timestamp={ timestamp }
enableIncrement={ !isLoading }
isLoading={ isLoading }
color="text_secondary"
......@@ -60,18 +60,18 @@ const LatestZkevmL2BatchItem = ({ batch, isLoading }: Props) => {
<Flex alignItems="center">
<Skeleton isLoaded={ !isLoading } mr={ 2 }>Txn</Skeleton>
<LinkInternal
href={ route({ pathname: '/batches/[number]', query: { number: batch.number.toString(), tab: 'txs' } }) }
href={ route({ pathname: '/batches/[number]', query: { number: number.toString(), tab: 'txs' } }) }
isLoading={ isLoading }
>
<Skeleton isLoaded={ !isLoading }>
{ batch.tx_count }
{ txCount }
</Skeleton>
</LinkInternal>
</Flex>
<ZkEvmL2TxnBatchStatus status={ batch.status } isLoading={ isLoading }/>
{ status }
</Flex>
</Box>
);
};
export default LatestZkevmL2BatchItem;
export default LatestBatchItem;
......@@ -14,8 +14,9 @@ import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
import { ZKEVM_L2_TXN_BATCHES_ITEM } from 'stubs/zkEvmL2';
import LinkInternal from 'ui/shared/links/LinkInternal';
import ZkEvmL2TxnBatchStatus from 'ui/shared/statusTag/ZkEvmL2TxnBatchStatus';
import LatestZkevmL2BatchItem from './LatestZkevmL2BatchItem';
import LatestBatchItem from './LatestBatchItem';
const LatestZkEvmL2Batches = () => {
const isMobile = useIsMobile();
......@@ -53,7 +54,7 @@ const LatestZkEvmL2Batches = () => {
let content;
if (isError) {
content = <Text>No data. Please reload page.</Text>;
content = <Text>No data. Please reload the page.</Text>;
}
if (data) {
......@@ -63,13 +64,19 @@ const LatestZkEvmL2Batches = () => {
<>
<VStack spacing={ 2 } mb={ 3 } overflow="hidden" alignItems="stretch">
<AnimatePresence initial={ false } >
{ dataToShow.map(((batch, index) => (
<LatestZkevmL2BatchItem
key={ batch.number + (isPlaceholderData ? String(index) : '') }
batch={ batch }
isLoading={ isPlaceholderData }
/>
))) }
{ dataToShow.map(((batch, index) => {
const status = <ZkEvmL2TxnBatchStatus status={ batch.status } isLoading={ isPlaceholderData }/>;
return (
<LatestBatchItem
key={ batch.number + (isPlaceholderData ? String(index) : '') }
number={ batch.number }
txCount={ batch.tx_count }
timestamp={ batch.timestamp }
status={ status }
isLoading={ isPlaceholderData }
/>
);
})) }
</AnimatePresence>
</VStack>
<Flex justifyContent="center">
......
import { Text } from '@chakra-ui/react';
import React from 'react';
import type { SocketMessage } from 'lib/socket/types';
import useApiQuery from 'lib/api/useApiQuery';
import useGradualIncrement from 'lib/hooks/useGradualIncrement';
import useIsMobile from 'lib/hooks/useIsMobile';
import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
import { ARBITRUM_MESSAGES_ITEM } from 'stubs/arbitrumL2';
import LatestDeposits from './LatestDeposits';
const LatestArbitrumDeposits = () => {
const isMobile = useIsMobile();
const itemsCount = isMobile ? 2 : 6;
const { data, isPlaceholderData, isError } = useApiQuery('homepage_arbitrum_deposits', {
queryOptions: {
placeholderData: { items: Array(itemsCount).fill(ARBITRUM_MESSAGES_ITEM) },
},
});
const [ num, setNum ] = useGradualIncrement(0);
const [ socketAlert, setSocketAlert ] = React.useState('');
const handleSocketClose = React.useCallback(() => {
setSocketAlert('Connection is lost. Please reload the page.');
}, []);
const handleSocketError = React.useCallback(() => {
setSocketAlert('An error has occurred while fetching new transactions. Please reload the page.');
}, []);
const handleNewDepositMessage: SocketMessage.NewArbitrumDeposits['handler'] = React.useCallback((payload) => {
setNum(payload.new_messages_to_rollup_amount);
}, [ setNum ]);
const channel = useSocketChannel({
topic: 'arbitrum:new_messages_to_rollup_amount',
onSocketClose: handleSocketClose,
onSocketError: handleSocketError,
isDisabled: false,
});
useSocketMessage({
channel,
event: 'new_messages_to_rollup_amount',
handler: handleNewDepositMessage,
});
if (isError) {
return <Text mt={ 4 }>No data. Please reload the page.</Text>;
}
if (data) {
return (
<LatestDeposits
items={ data.items.slice(0, itemsCount).map((item) => (
{
l1BlockNumber: item.origination_transaction_block_number,
l1TxHash: item.origination_transaction_hash,
l2TxHash: item.completion_transaction_hash,
timestamp: item.origination_timestamp,
}
)) }
isLoading={ isPlaceholderData }
socketItemsNum={ num }
socketAlert={ socketAlert }
/>
);
}
return null;
};
export default LatestArbitrumDeposits;
import {
Box,
Flex,
Grid,
Skeleton,
} from '@chakra-ui/react';
import React from 'react';
import { route } from 'nextjs-routes';
import useIsMobile from 'lib/hooks/useIsMobile';
import BlockEntityL1 from 'ui/shared/entities/block/BlockEntityL1';
import TxEntity from 'ui/shared/entities/tx/TxEntity';
import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
import LinkInternal from 'ui/shared/links/LinkInternal';
import SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice';
import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip';
type DepositsItem = {
l1BlockNumber: number;
l1TxHash: string;
l2TxHash: string | null;
timestamp: string | null;
}
type Props = {
isLoading?: boolean;
items: Array<DepositsItem>;
socketItemsNum: number;
socketAlert?: string;
}
type ItemProps = {
item: DepositsItem;
isLoading?: boolean;
}
const LatestDepositsItem = ({ item, isLoading }: ItemProps) => {
const isMobile = useIsMobile();
const l1BlockLink = (
<BlockEntityL1
number={ item.l1BlockNumber }
isLoading={ isLoading }
fontSize="sm"
lineHeight={ 5 }
fontWeight={ 700 }
/>
);
const l1TxLink = (
<TxEntityL1
isLoading={ isLoading }
hash={ item.l1TxHash }
fontSize="sm"
lineHeight={ 5 }
truncation={ isMobile ? 'constant_long' : 'dynamic' }
/>
);
const l2TxLink = item.l2TxHash ? (
<TxEntity
isLoading={ isLoading }
hash={ item.l2TxHash }
fontSize="sm"
lineHeight={ 5 }
truncation={ isMobile ? 'constant_long' : 'dynamic' }
/>
) : null;
const content = (() => {
if (isMobile) {
return (
<>
<Flex justifyContent="space-between" alignItems="center" mb={ 1 }>
{ l1BlockLink }
<TimeAgoWithTooltip
timestamp={ item.timestamp }
isLoading={ isLoading }
color="text_secondary"
/>
</Flex>
<Grid gridTemplateColumns="56px auto">
<Skeleton isLoaded={ !isLoading } my="5px" w="fit-content">
L1 txn
</Skeleton>
{ l1TxLink }
<Skeleton isLoaded={ !isLoading } my="3px" w="fit-content">
L2 txn
</Skeleton>
{ l2TxLink }
</Grid>
</>
);
}
return (
<Grid width="100%" columnGap={ 4 } rowGap={ 2 } templateColumns="max-content max-content auto" w="100%">
{ l1BlockLink }
<Skeleton isLoaded={ !isLoading } w="fit-content" h="fit-content" my="5px">
L1 txn
</Skeleton>
{ l1TxLink }
<TimeAgoWithTooltip
timestamp={ item.timestamp }
isLoading={ isLoading }
color="text_secondary"
w="fit-content"
h="fit-content"
my="2px"
/>
<Skeleton isLoaded={ !isLoading } w="fit-content" h="fit-content" my="2px">
L2 txn
</Skeleton>
{ l2TxLink }
</Grid>
);
})();
return (
<Box
width="100%"
borderTop="1px solid"
borderColor="divider"
py={ 4 }
px={{ base: 0, lg: 4 }}
_last={{ borderBottom: '1px solid', borderColor: 'divider' }}
fontSize="sm"
lineHeight={ 5 }
>
{ content }
</Box>
);
};
const LatestDeposits = ({ isLoading, items, socketAlert, socketItemsNum }: Props) => {
const depositsUrl = route({ pathname: '/deposits' });
return (
<>
<SocketNewItemsNotice borderBottomRadius={ 0 } url={ depositsUrl } num={ socketItemsNum } alert={ socketAlert } type="deposit" isLoading={ isLoading }/>
<Box mb={{ base: 3, lg: 4 }}>
{ items.map(((item, index) => (
<LatestDepositsItem
key={ item.l1TxHash + item.l2TxHash + (isLoading ? index : '') }
item={ item }
isLoading={ isLoading }
/>
))) }
</Box>
<Flex justifyContent="center">
<LinkInternal fontSize="sm" href={ depositsUrl }>View all deposits</LinkInternal>
</Flex>
</>
);
};
export default LatestDeposits;
......@@ -6,32 +6,26 @@ import {
} from '@chakra-ui/react';
import React from 'react';
import type { OptimisticL2DepositsItem } from 'types/api/optimisticL2';
import config from 'configs/app';
import useIsMobile from 'lib/hooks/useIsMobile';
import BlockEntityL1 from 'ui/shared/entities/block/BlockEntityL1';
import TxEntity from 'ui/shared/entities/tx/TxEntity';
import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip';
const feature = config.features.rollup;
type Props = {
item: OptimisticL2DepositsItem;
l1BlockNumber: number;
l1TxHash: string;
l2TxHash: string | null;
timestamp: string | null;
isLoading?: boolean;
}
const LatestDepositsItem = ({ item, isLoading }: Props) => {
const LatestDepositsItem = ({ l1BlockNumber, l1TxHash, l2TxHash, timestamp, isLoading }: Props) => {
const isMobile = useIsMobile();
if (!feature.isEnabled || feature.type !== 'optimistic') {
return null;
}
const l1BlockLink = (
<BlockEntityL1
number={ item.l1_block_number }
number={ l1BlockNumber }
isLoading={ isLoading }
fontSize="sm"
lineHeight={ 5 }
......@@ -42,22 +36,22 @@ const LatestDepositsItem = ({ item, isLoading }: Props) => {
const l1TxLink = (
<TxEntityL1
isLoading={ isLoading }
hash={ item.l1_tx_hash }
hash={ l1TxHash }
fontSize="sm"
lineHeight={ 5 }
truncation={ isMobile ? 'constant_long' : 'dynamic' }
/>
);
const l2TxLink = (
const l2TxLink = l2TxHash ? (
<TxEntity
isLoading={ isLoading }
hash={ item.l2_tx_hash }
hash={ l2TxHash }
fontSize="sm"
lineHeight={ 5 }
truncation={ isMobile ? 'constant_long' : 'dynamic' }
/>
);
) : null;
const content = (() => {
if (isMobile) {
......@@ -66,7 +60,7 @@ const LatestDepositsItem = ({ item, isLoading }: Props) => {
<Flex justifyContent="space-between" alignItems="center" mb={ 1 }>
{ l1BlockLink }
<TimeAgoWithTooltip
timestamp={ item.l1_block_timestamp }
timestamp={ timestamp }
isLoading={ isLoading }
color="text_secondary"
/>
......@@ -93,7 +87,7 @@ const LatestDepositsItem = ({ item, isLoading }: Props) => {
</Skeleton>
{ l1TxLink }
<TimeAgoWithTooltip
timestamp={ item.l1_block_timestamp }
timestamp={ timestamp }
isLoading={ isLoading }
color="text_secondary"
w="fit-content"
......
......@@ -4,11 +4,11 @@ import * as depositMock from 'mocks/l2deposits/deposits';
import { ENVS_MAP } from 'playwright/fixtures/mockEnvs';
import { test, expect } from 'playwright/lib';
import LatestDeposits from './LatestDeposits';
import LatestOptimisticDeposits from './LatestOptimisticDeposits';
test('default view +@mobile +@dark-mode', async({ render, mockApiResponse, mockEnvs }) => {
await mockEnvs(ENVS_MAP.optimisticRollup);
mockApiResponse('homepage_deposits', depositMock.data.items);
const component = await render(<LatestDeposits/>);
mockApiResponse('homepage_optimistic_deposits', depositMock.data.items);
const component = await render(<LatestOptimisticDeposits/>);
await expect(component).toHaveScreenshot();
});
......@@ -42,7 +42,7 @@ const ContractListModal = ({ onClose, onBack, type, contracts }: Props) => {
return contracts
.filter((contract) => Boolean(contract.solidityScanReport))
.sort((a, b) =>
(parseFloat(b.solidityScanReport?.scan_summary.score_v2 ?? '0')) - (parseFloat(a.solidityScanReport?.scan_summary.score_v2 ?? '0')),
(parseFloat(b.solidityScanReport?.scan_summary?.score_v2 ?? '0')) - (parseFloat(a.solidityScanReport?.scan_summary?.score_v2 ?? '0')),
);
case ContractListTypes.VERIFIED:
return contracts.filter((contract) => contract.isVerified);
......
......@@ -27,7 +27,7 @@ const ContractSecurityReport = ({ securityReport }: Props) => {
onToggle();
}, [ onToggle ]);
if (!securityReport) {
if (!securityReport?.scan_summary?.score_v2) {
return null;
}
......
......@@ -13,16 +13,23 @@ const addresses: AddressesResponse = {
...addressMocks.withName,
tx_count: '1',
coin_balance: '12345678901234567890000',
}, {
},
{
...addressMocks.token,
tx_count: '109123890123',
coin_balance: '22222345678901234567890000',
ens_domain_name: null,
}, {
},
{
...addressMocks.withoutName,
tx_count: '11',
coin_balance: '1000000000000000000',
},
{
...addressMocks.eoa,
tx_count: '420',
coin_balance: '123456',
},
],
total_supply: '25222000',
next_page_params: null,
......
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