Commit 5b1871cc authored by Max Alekseenko's avatar Max Alekseenko

Merge branch 'main' into rework-marketplace-app-header

parents 847ec0a2 b451db80
import app from './app';
import { getEnvValue, getExternalAssetFilePath } from './utils';
const defaultImageUrl = app.baseUrl + '/static/og_placeholder.png';
const defaultImageUrl = '/static/og_placeholder.png';
const meta = Object.freeze({
promoteBlockscoutInTitle: getEnvValue('NEXT_PUBLIC_PROMOTE_BLOCKSCOUT_IN_TITLE') || 'true',
og: {
description: getEnvValue('NEXT_PUBLIC_OG_DESCRIPTION') || '',
imageUrl: getExternalAssetFilePath('NEXT_PUBLIC_OG_IMAGE_URL') || defaultImageUrl,
imageUrl: app.baseUrl + (getExternalAssetFilePath('NEXT_PUBLIC_OG_IMAGE_URL') || defaultImageUrl),
},
});
......
......@@ -29,6 +29,7 @@ import type {
AddressNFTsResponse,
AddressCollectionsResponse,
AddressNFTTokensFilter,
AddressCoinBalanceHistoryChartOld,
} from 'types/api/address';
import type { AddressesResponse } from 'types/api/addresses';
import type { TxBlobs, Blob } from 'types/api/blobs';
......@@ -859,7 +860,7 @@ Q extends 'address_internal_txs' ? AddressInternalTxsResponse :
Q extends 'address_token_transfers' ? AddressTokenTransferResponse :
Q extends 'address_blocks_validated' ? AddressBlocksValidatedResponse :
Q extends 'address_coin_balance' ? AddressCoinBalanceHistoryResponse :
Q extends 'address_coin_balance_chart' ? AddressCoinBalanceHistoryChart :
Q extends 'address_coin_balance_chart' ? AddressCoinBalanceHistoryChartOld | AddressCoinBalanceHistoryChart :
Q extends 'address_logs' ? LogsResponseAddress :
Q extends 'address_tokens' ? AddressTokensResponse :
Q extends 'address_nfts' ? AddressNFTsResponse :
......
import { useEffect } from 'react';
import type { AdBannerProviders } from 'types/client/adProviders';
import config from 'configs/app';
import { useAppContext } from 'lib/contexts/app';
import * as cookies from 'lib/cookies';
import isBrowser from 'lib/isBrowser';
const DEFAULT_URL = 'https://request-global.czilladx.com';
// in general, detect should work with any ad-provider url (that is alive)
// but we see some false-positive results in certain browsers
const TEST_URLS: Record<AdBannerProviders, string> = {
slise: 'https://v1.slise.xyz/serve',
coinzilla: 'https://request-global.czilladx.com',
adbutler: 'https://servedbyadbutler.com/app.js',
hype: 'https://api.hypelab.com/v1/scripts/hp-sdk.js',
// I don't have an url for getit to test
getit: DEFAULT_URL,
none: DEFAULT_URL,
};
const feature = config.features.adsBanner;
export default function useAdblockDetect() {
const hasAdblockCookie = cookies.get(cookies.NAMES.ADBLOCK_DETECTED, useAppContext().cookies);
const provider = feature.isEnabled && feature.provider;
useEffect(() => {
if (isBrowser() && !hasAdblockCookie) {
const url = 'https://request-global.czilladx.com';
if (isBrowser() && !hasAdblockCookie && provider) {
const url = TEST_URLS[provider] || DEFAULT_URL;
fetch(url, {
method: 'HEAD',
mode: 'no-cors',
......
......@@ -35,33 +35,36 @@ export const baseResponse: AddressCoinBalanceHistoryResponse = {
next_page_params: null,
};
export const chartResponse: AddressCoinBalanceHistoryChart = [
{
date: '2022-11-02',
value: '128238612887883515',
},
{
date: '2022-11-03',
value: '199807583157570922',
},
{
date: '2022-11-04',
value: '114487912907005778',
},
{
date: '2022-11-05',
value: '219533112907005778',
},
{
date: '2022-11-06',
value: '116487912907005778',
},
{
date: '2022-11-07',
value: '199807583157570922',
},
{
date: '2022-11-08',
value: '216488112907005778',
},
];
export const chartResponse: AddressCoinBalanceHistoryChart = {
items: [
{
date: '2022-11-02',
value: '128238612887883515',
},
{
date: '2022-11-03',
value: '199807583157570922',
},
{
date: '2022-11-04',
value: '114487912907005778',
},
{
date: '2022-11-05',
value: '219533112907005778',
},
{
date: '2022-11-06',
value: '116487912907005778',
},
{
date: '2022-11-07',
value: '199807583157570922',
},
{
date: '2022-11-08',
value: '216488112907005778',
},
],
days: 10,
};
......@@ -3,6 +3,7 @@ import React from 'react';
import type { Route } from 'nextjs-routes';
import config from 'configs/app';
import useAdblockDetect from 'lib/hooks/useAdblockDetect';
import useGetCsrfToken from 'lib/hooks/useGetCsrfToken';
import * as metadata from 'lib/metadata';
......@@ -34,9 +35,14 @@ const PageNextJs = (props: Props) => {
<meta property="og:title" content={ opengraph.title }/>
{ opengraph.description && <meta property="og:description" content={ opengraph.description }/> }
<meta property="og:image" content={ opengraph.imageUrl }/>
<meta property="og:type" content="website"/>
{ /* Twitter Meta Tags */ }
<meta name="twitter:card" content="summary_large_image"/>
<meta property="twitter:domain" content={ config.app.host }/>
<meta name="twitter:title" content={ opengraph.title }/>
{ opengraph.description && <meta name="twitter:description" content={ opengraph.description }/> }
<meta property="twitter:image" content={ opengraph.imageUrl }/>
<meta property="og:type" content="website"/>
</Head>
{ props.children }
</>
......
......@@ -13,12 +13,16 @@ export function ad(): CspDev.DirectiveDescriptor {
'*.coinzilla.com',
'https://request-global.czilladx.com',
// adbutler
'servedbyadbutler.com',
// slise
'*.slise.xyz',
// hype
'api.hypelab.com',
'*.ixncdn.com',
'*.cloudfront.net',
//getit
'v1.getittech.io',
......
......@@ -148,11 +148,20 @@ export interface AddressCoinBalanceHistoryResponse {
} | null;
}
export type AddressCoinBalanceHistoryChart = Array<{
// remove after api release
export type AddressCoinBalanceHistoryChartOld = Array<{
date: string;
value: string;
}>
export type AddressCoinBalanceHistoryChart = {
items: Array<{
date: string;
value: string;
}>;
days: number;
};
export interface AddressBlocksValidatedResponse {
items: Array<Block>;
next_page_params: {
......
......@@ -20,6 +20,7 @@ export interface NovesClassificationData {
type: string | null;
};
message?: string;
deployedContractAddress?: string;
}
export interface Approved {
......
......@@ -15,10 +15,17 @@ const AddressCoinBalanceChart = ({ addressHash }: Props) => {
pathParams: { hash: addressHash },
});
const items = React.useMemo(() => data?.map(({ date, value }) => ({
date: new Date(date),
value: BigNumber(value).div(10 ** config.chain.currency.decimals).toNumber(),
})), [ data ]);
const items = React.useMemo(() => {
if (!data) {
return undefined;
}
const dataItems = 'items' in data ? data.items : data;
return dataItems.map(({ date, value }) => ({
date: new Date(date),
value: BigNumber(value).div(10 ** config.chain.currency.decimals).toNumber(),
}));
}, [ data ]);
return (
<ChartWidget
......@@ -28,6 +35,7 @@ const AddressCoinBalanceChart = ({ addressHash }: Props) => {
isLoading={ isPending }
h="300px"
units={ currencyUnits.ether }
emptyText={ data && 'days' in data && `Insufficient data for the past ${ data.days } days` }
/>
);
};
......
import { Flex, Skeleton, Button, Grid, GridItem, Alert, Link, chakra, Box } from '@chakra-ui/react';
import { Flex, Skeleton, Button, Grid, GridItem, Alert, Link, chakra, Box, useColorModeValue } from '@chakra-ui/react';
import { useQueryClient } from '@tanstack/react-query';
import React from 'react';
......@@ -16,6 +16,7 @@ import useSocketMessage from 'lib/socket/useSocketMessage';
import * as stubs from 'stubs/contract';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import Hint from 'ui/shared/Hint';
import LinkExternal from 'ui/shared/LinkExternal';
import LinkInternal from 'ui/shared/LinkInternal';
import RawDataSnippet from 'ui/shared/RawDataSnippet';
......@@ -34,11 +35,24 @@ type InfoItemProps = {
content: string | React.ReactNode;
className?: string;
isLoading: boolean;
hint?: string;
}
const InfoItem = chakra(({ label, content, className, isLoading }: InfoItemProps) => (
const InfoItem = chakra(({ label, content, hint, className, isLoading }: InfoItemProps) => (
<GridItem display="flex" columnGap={ 6 } wordBreak="break-all" className={ className } alignItems="baseline">
<Skeleton isLoaded={ !isLoading } w="170px" flexShrink={ 0 } fontWeight={ 500 }>{ label }</Skeleton>
<Skeleton isLoaded={ !isLoading } w="170px" flexShrink={ 0 } fontWeight={ 500 }>
<Flex alignItems="center">
{ label }
{ hint && (
<Hint
label={ hint }
ml={ 2 }
color={ useColorModeValue('gray.600', 'gray.400') }
tooltipProps={{ placement: 'bottom' }}
/>
) }
</Flex>
</Skeleton>
<Skeleton isLoaded={ !isLoading }>{ content }</Skeleton>
</GridItem>
));
......@@ -251,7 +265,14 @@ const ContractCode = ({ addressHash, noSocket }: Props) => {
{ data.name && <InfoItem label="Contract name" content={ data.name } isLoading={ isPlaceholderData }/> }
{ data.compiler_version && <InfoItem label="Compiler version" content={ data.compiler_version } isLoading={ isPlaceholderData }/> }
{ data.evm_version && <InfoItem label="EVM version" content={ data.evm_version } textTransform="capitalize" isLoading={ isPlaceholderData }/> }
{ licenseLink && <InfoItem label="License" content={ licenseLink } isLoading={ isPlaceholderData }/> }
{ licenseLink && (
<InfoItem
label="License"
content={ licenseLink }
hint="License type is informative field, and initial source code might have different license type from displayed."
isLoading={ isPlaceholderData }
/>
) }
{ typeof data.optimization_enabled === 'boolean' &&
<InfoItem label="Optimization enabled" content={ data.optimization_enabled ? 'true' : 'false' } isLoading={ isPlaceholderData }/> }
{ data.optimization_runs && <InfoItem label="Optimization runs" content={ String(data.optimization_runs) } isLoading={ isPlaceholderData }/> }
......
import { Box, Button, chakra, Flex, Grid, Hide, Popover, PopoverBody, PopoverContent, PopoverTrigger, Show, Skeleton, useDisclosure } from '@chakra-ui/react';
import {
Box,
Button,
Flex,
Grid,
Hide,
Popover,
PopoverBody,
PopoverContent,
PopoverTrigger,
Show,
Skeleton,
useDisclosure,
chakra,
} from '@chakra-ui/react';
import _clamp from 'lodash/clamp';
import React from 'react';
......@@ -12,6 +26,7 @@ import dayjs from 'lib/date/dayjs';
import EnsEntity from 'ui/shared/entities/ens/EnsEntity';
import IconSvg from 'ui/shared/IconSvg';
import LinkInternal from 'ui/shared/LinkInternal';
import PopoverTriggerTooltip from 'ui/shared/PopoverTriggerTooltip';
interface Props {
addressHash: string;
......@@ -90,25 +105,27 @@ const AddressEnsDomains = ({ addressHash, mainDomainName }: Props) => {
return (
<Popover isOpen={ isOpen } onClose={ onClose } placement="bottom-start" isLazy>
<PopoverTrigger>
<Button
size="sm"
variant="outline"
colorScheme="gray"
onClick={ onToggle }
aria-label="Address domains"
fontWeight={ 500 }
px={ 2 }
h="32px"
flexShrink={ 0 }
>
<IconSvg name="ENS_slim" boxSize={ 5 }/>
<Show above="xl">
<chakra.span ml={ 1 }>{ totalRecords } Domain{ data.items.length > 1 ? 's' : '' }</chakra.span>
</Show>
<Hide above="xl">
<chakra.span ml={ 1 }>{ totalRecords }</chakra.span>
</Hide>
</Button>
<PopoverTriggerTooltip label="List of names resolved or owned by this address">
<Button
size="sm"
variant="outline"
colorScheme="gray"
onClick={ onToggle }
aria-label="Address domains"
fontWeight={ 500 }
px={ 2 }
h="32px"
flexShrink={ 0 }
>
<IconSvg name="ENS_slim" boxSize={ 5 }/>
<Show above="xl">
<chakra.span ml={ 1 }>{ totalRecords } Domain{ data.items.length > 1 ? 's' : '' }</chakra.span>
</Show>
<Hide above="xl">
<chakra.span ml={ 1 }>{ totalRecords }</chakra.span>
</Hide>
</Button>
</PopoverTriggerTooltip>
</PopoverTrigger>
<PopoverContent w={{ base: '100vw', lg: '500px' }}>
<PopoverBody px={ 6 } py={ 5 } fontSize="sm" display="flex" flexDir="column" rowGap={ 5 } alignItems="flex-start">
......
......@@ -66,7 +66,7 @@ const BlockPageContent = () => {
</>
),
},
blockQuery.data?.blob_tx_count ?
config.features.dataAvailability.isEnabled && blockQuery.data?.blob_tx_count ?
{
id: 'blob_txs',
title: 'Blob txns',
......
......@@ -39,7 +39,7 @@ const TransactionPageContent = () => {
const txQuery = useTxQuery();
const { data, isPlaceholderData, isError, error, errorUpdateCount } = txQuery;
const showDegradedView = publicClient && (isError || isPlaceholderData) && errorUpdateCount > 0;
const showDegradedView = publicClient && ((isError && error.status !== 422) || isPlaceholderData) && errorUpdateCount > 0;
const tabs: Array<RoutedTab> = (() => {
const detailsComponent = showDegradedView ?
......@@ -63,7 +63,7 @@ const TransactionPageContent = () => {
{ id: 'user_ops', title: 'User operations', component: <TxUserOps txQuery={ txQuery }/> } :
undefined,
{ id: 'internal', title: 'Internal txns', component: <TxInternals txQuery={ txQuery }/> },
txQuery.data?.blob_versioned_hashes?.length ?
config.features.dataAvailability.isEnabled && txQuery.data?.blob_versioned_hashes?.length ?
{ id: 'blobs', title: 'Blobs', component: <TxBlobs txQuery={ txQuery }/> } :
undefined,
{ id: 'logs', title: 'Logs', component: <TxLogs txQuery={ txQuery }/> },
......
......@@ -29,12 +29,6 @@ const Transactions = () => {
const isMobile = useIsMobile();
const tab = getQueryParamString(router.query.tab);
React.useEffect(() => {
if (tab === 'blob_txs' && !config.features.dataAvailability.isEnabled) {
router.replace({ pathname: '/txs' }, undefined, { shallow: true });
}
}, [ router, tab ]);
const txsValidatedQuery = useQueryWithPages({
resourceName: 'txs_validated',
filters: { filter: 'validated' },
......
......@@ -20,6 +20,7 @@ import config from 'configs/app';
import stripTrailingSlash from 'lib/stripTrailingSlash';
import IconSvg from 'ui/shared/IconSvg';
import LinkExternal from 'ui/shared/LinkExternal';
import PopoverTriggerTooltip from 'ui/shared/PopoverTriggerTooltip';
interface Props {
className?: string;
......@@ -55,26 +56,27 @@ const NetworkExplorers = ({ className, type, pathParam }: Props) => {
return (
<Popover isOpen={ isOpen } onClose={ onClose } placement="bottom-start" isLazy>
<PopoverTrigger>
<Button
className={ className }
size="sm"
variant="outline"
colorScheme="gray"
onClick={ onToggle }
aria-label="Verify in other explorers"
fontWeight={ 500 }
px={ 2 }
h="32px"
flexShrink={ 0 }
>
<IconSvg name="explorer" boxSize={ 5 }/>
<Show above="xl">
<chakra.span ml={ 1 }>{ explorersLinks.length } Explorer{ explorersLinks.length > 1 ? 's' : '' }</chakra.span>
</Show>
<Hide above="xl">
<chakra.span ml={ 1 }>{ explorersLinks.length }</chakra.span>
</Hide>
</Button>
<PopoverTriggerTooltip label="Verify with other explorers" className={ className }>
<Button
size="sm"
variant="outline"
colorScheme="gray"
onClick={ onToggle }
aria-label="Verify in other explorers"
fontWeight={ 500 }
px={ 2 }
h="32px"
flexShrink={ 0 }
>
<IconSvg name="explorer" boxSize={ 5 }/>
<Show above="xl">
<chakra.span ml={ 1 }>{ explorersLinks.length } Explorer{ explorersLinks.length > 1 ? 's' : '' }</chakra.span>
</Show>
<Hide above="xl">
<chakra.span ml={ 1 }>{ explorersLinks.length }</chakra.span>
</Hide>
</Button>
</PopoverTriggerTooltip>
</PopoverTrigger>
<PopoverContent w="auto">
<PopoverBody >
......
import { Skeleton, Tooltip, chakra } from '@chakra-ui/react';
import React from 'react';
import useIsMobile from 'lib/hooks/useIsMobile';
type Props = {
label: string;
isLoading?: boolean;
className?: string;
children: React.ReactNode;
}
const PopoverTriggerTooltip = ({ label, isLoading, className, children }: Props, ref: React.ForwardedRef<HTMLDivElement>) => {
const isMobile = useIsMobile();
return (
// tooltip need to be wrapped in div for proper popover positioning
<Skeleton isLoaded={ !isLoading } borderRadius="base" ref={ ref } className={ className }>
<Tooltip
label={ label }
isDisabled={ isMobile }
// need a delay to avoid flickering when closing the popover
openDelay={ 100 }
>
{ children }
</Tooltip>
</Skeleton>
);
};
export default chakra(React.forwardRef(PopoverTriggerTooltip));
......@@ -35,11 +35,12 @@ export type Props = {
isLoading: boolean;
className?: string;
isError: boolean;
emptyText?: string;
}
const DOWNLOAD_IMAGE_SCALE = 5;
const ChartWidget = ({ items, title, description, isLoading, className, isError, units }: Props) => {
const ChartWidget = ({ items, title, description, isLoading, className, isError, units, emptyText }: Props) => {
const ref = useRef<HTMLDivElement>(null);
const [ isFullscreen, setIsFullscreen ] = useState(false);
const [ isZoomResetInitial, setIsZoomResetInitial ] = React.useState(true);
......@@ -134,7 +135,7 @@ const ChartWidget = ({ items, title, description, isLoading, className, isError,
if (!hasItems) {
return (
<Center flexGrow={ 1 }>
<Text variant="secondary" fontSize="sm">No data</Text>
<Text variant="secondary" fontSize="sm">{ emptyText || 'No data' }</Text>
</Center>
);
}
......
import { Button, Skeleton } from '@chakra-ui/react';
import { Button } from '@chakra-ui/react';
import React from 'react';
import IconSvg from 'ui/shared/IconSvg';
import PopoverTriggerTooltip from '../PopoverTriggerTooltip';
import useScoreLevelAndColor from './useScoreLevelAndColor';
interface Props {
......@@ -21,10 +22,9 @@ const SolidityscanReportButton = (
const { scoreColor } = useScoreLevelAndColor(score);
return (
<Skeleton isLoaded={ !isLoading } borderRadius="base">
<PopoverTriggerTooltip label="Security score" isLoading={ isLoading } className={ className }>
<Button
ref={ ref }
className={ className }
color={ scoreColor }
size="sm"
variant="outline"
......@@ -39,7 +39,7 @@ const SolidityscanReportButton = (
<IconSvg name={ score < 80 ? 'score/score-not-ok' : 'score/score-ok' } boxSize={ 5 } mr={ onlyIcon ? 0 : 1 }/>
{ onlyIcon ? null : score }
</Button>
</Skeleton>
</PopoverTriggerTooltip>
);
};
......
......@@ -57,6 +57,10 @@ function extractAddresses(data: NovesResponseData) {
addressesSet.add({ hash: data.classificationData.approved.spender });
}
if (data.classificationData.deployedContractAddress) {
addressesSet.add({ hash: data.classificationData.deployedContractAddress });
}
if (data.txTypeVersion === 2) {
data.classificationData.sent.forEach((transaction) => {
addressesSet.add({ hash: transaction.from.address, name: transaction.from.name });
......@@ -72,5 +76,5 @@ function extractAddresses(data: NovesResponseData) {
const addresses = Array.from(addressesSet) as Array<{hash: string; name?: string}>; // Convert Set to an array
// Remove empty and null values
return addresses.filter(address => address.hash !== null && address.hash !== '');
return addresses.filter(address => address.hash !== null && address.hash !== '' && address.hash !== undefined);
}
......@@ -4,6 +4,7 @@ import React from 'react';
import type { OptimisticL2WithdrawalStatus } from 'types/api/optimisticL2';
import { WITHDRAWAL_STATUSES } from 'types/api/optimisticL2';
import config from 'configs/app';
import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
import VerificationSteps from 'ui/shared/verificationSteps/VerificationSteps';
......@@ -12,8 +13,10 @@ interface Props {
l1TxHash: string | undefined;
}
const rollupFeature = config.features.rollup;
const TxDetailsWithdrawalStatus = ({ status, l1TxHash }: Props) => {
if (!status || !WITHDRAWAL_STATUSES.includes(status)) {
if (!status || !WITHDRAWAL_STATUSES.includes(status) || !rollupFeature.isEnabled || rollupFeature.type !== 'optimistic') {
return null;
}
......@@ -46,7 +49,7 @@ const TxDetailsWithdrawalStatus = ({ status, l1TxHash }: Props) => {
variant="outline"
size="sm"
as="a"
href="https://app.optimism.io/bridge/withdraw"
href={ rollupFeature.L2WithdrawalUrl }
target="_blank"
>
Claim funds
......
......@@ -8,7 +8,7 @@ import TxInternalsListItem from 'ui/tx/internals/TxInternalsListItem';
const TxInternalsList = ({ data, isLoading }: { data: Array<InternalTransaction>; isLoading?: boolean }) => {
return (
<Box>
{ data.map((item, index) => <TxInternalsListItem key={ item.transaction_hash + (isLoading ? index : '') } { ...item } isLoading={ isLoading }/>) }
{ data.map((item, index) => <TxInternalsListItem key={ item.index.toString() + (isLoading ? index : '') } { ...item } isLoading={ isLoading }/>) }
</Box>
);
};
......
......@@ -44,7 +44,7 @@ const TxInternalsTable = ({ data, sort, onSortToggle, top, isLoading }: Props) =
</Thead>
<Tbody>
{ data.map((item, index) => (
<TxInternalsTableItem key={ item.transaction_hash + (isLoading ? index : '') } { ...item } isLoading={ isLoading }/>
<TxInternalsTableItem key={ item.index.toString() + (isLoading ? index : '') } { ...item } isLoading={ isLoading }/>
)) }
</Tbody>
</Table>
......
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