Commit 89b91fc2 authored by Max Alekseenko's avatar Max Alekseenko

create contract list modal

parent 37f66880
......@@ -28,3 +28,9 @@ export enum MarketplaceCategory {
ALL = 'All',
FAVORITES = 'Favorites',
}
export enum ContractListTypes {
ANALYZED = 'Analyzed',
ALL = 'All',
VERIFIED = 'Verified',
}
import {
Grid, Modal, ModalBody,
ModalCloseButton, ModalContent, ModalHeader, ModalOverlay,
} from '@chakra-ui/react';
import React from 'react';
import { ContractListTypes } from 'types/client/marketplace';
import useIsMobile from 'lib/hooks/useIsMobile';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import ContractSecurityReport from './ContractSecurityReport';
type Props = {
onClose: () => void;
type: ContractListTypes;
contracts: Array<any>; // eslint-disable-line @typescript-eslint/no-explicit-any
}
const ContractListModal = ({ onClose, type, contracts }: Props) => {
const isMobile = useIsMobile();
const displayedContracts = React.useMemo(() => {
switch (type) {
default:
case ContractListTypes.ALL:
return contracts;
case ContractListTypes.ANALYZED:
return contracts
.filter((contract) => Boolean(contract.solidityScanReport))
.sort((a, b) => b.solidityScanReport.scan_summary.score_v2 - a.solidityScanReport.scan_summary.score_v2);
case ContractListTypes.VERIFIED:
return contracts.filter((contract) => contract.isVerified);
}
}, [ contracts, type ]);
return (
<Modal
isOpen={ Boolean(type) }
onClose={ onClose }
size={ isMobile ? 'full' : 'md' }
isCentered
>
<ModalOverlay/>
<ModalContent>
<ModalHeader fontWeight="500" textStyle="h3" mb={ 4 }>Contracts</ModalHeader>
<ModalCloseButton/>
<ModalBody maxH="352px" overflow="scroll" mb={ 0 } display="flex" flexDirection="column" gap={ 2 }>
{ displayedContracts.map((contract) => (
<Grid key={ contract.address } height={ 8 } alignItems="center" gap={ 6 } templateColumns="max-content 1fr">
{ type === ContractListTypes.ANALYZED && (
<ContractSecurityReport securityReport={ contract.solidityScanReport }/>
) }
<AddressEntity
address={{
hash: contract.address,
name: contract.solidityScanReport?.contractname,
is_contract: true,
is_verified: contract.isVerified,
}}
noCopy
/>
</Grid>
)) }
</ModalBody>
</ModalContent>
</Modal>
);
};
export default ContractListModal;
import { Box, Text } from '@chakra-ui/react';
import React from 'react';
import config from 'configs/app';
import LinkExternal from 'ui/shared/LinkExternal';
import SolidityscanReportButton from 'ui/shared/solidityscanReport/SolidityscanReportButton';
import SolidityscanReportDetails from 'ui/shared/solidityscanReport/SolidityscanReportDetails';
import SolidityscanReportScore from 'ui/shared/solidityscanReport/SolidityscanReportScore';
type Props = {
securityReport?: any; // eslint-disable-line @typescript-eslint/no-explicit-any
}
const ContractSecurityReport = ({ securityReport }: Props) => {
const {
scanner_reference_url: url,
scan_summary: {
score_v2: securityScore,
issue_severity_distribution: issueSeverityDistribution,
},
} = securityReport;
const totalIssues = Object.values(issueSeverityDistribution as Record<string, number>).reduce((acc, val) => acc + val, 0);
return (
<SolidityscanReportButton
score={ securityScore }
popoverContent={ (
<>
<Box mb={ 5 }>
The security score was derived from evaluating the smart contracts of a protocol on the { config.chain.name } network.
</Box>
<SolidityscanReportScore score={ securityScore }/>
{ issueSeverityDistribution && totalIssues > 0 && (
<Box mb={ 5 }>
<Text py="7px" variant="secondary" fontSize="xs" fontWeight={ 500 }>Threat score & vulnerabilities</Text>
<SolidityscanReportDetails vulnerabilities={ issueSeverityDistribution } vulnerabilitiesCount={ totalIssues }/>
</Box>
) }
<LinkExternal href={ url }>View full report</LinkExternal>
</>
) }
/>
);
};
export default ContractSecurityReport;
......@@ -2,7 +2,7 @@ import { Hide, Show } from '@chakra-ui/react';
import React from 'react';
import type { MouseEvent } from 'react';
import type { MarketplaceAppPreview } from 'types/client/marketplace';
import type { MarketplaceAppPreview, ContractListTypes } from 'types/client/marketplace';
import { MarketplaceCategory } from 'types/client/marketplace';
import config from 'configs/app';
......@@ -23,6 +23,7 @@ interface Props {
selectedCategoryId?: string;
onAppClick: (event: MouseEvent, id: string) => void;
securityReports: Array<any> | undefined; // eslint-disable-line @typescript-eslint/no-explicit-any
showContractList: (id: string, type: ContractListTypes) => void;
}
const MarketplaceListWithScores = ({
......@@ -34,6 +35,7 @@ const MarketplaceListWithScores = ({
selectedCategoryId,
onAppClick,
securityReports = [],
showContractList,
}: Props) => {
const displayedApps = React.useMemo(() =>
......@@ -63,6 +65,7 @@ const MarketplaceListWithScores = ({
onFavoriteClick={ onFavoriteClick }
isLoading={ isLoading }
onAppClick={ onAppClick }
showContractList={ showContractList }
/>
)) }
</Show>
......@@ -74,6 +77,7 @@ const MarketplaceListWithScores = ({
favoriteApps={ favoriteApps }
onFavoriteClick={ onFavoriteClick }
onInfoClick={ showAppInfo }
showContractList={ showContractList }
/>
</Hide>
</>
......
import { Box, Text } from '@chakra-ui/react';
import { Box, Text, Link } from '@chakra-ui/react';
import React from 'react';
import { ContractListTypes } from 'types/client/marketplace';
import config from 'configs/app';
import { apos } from 'lib/html-entities';
import LinkExternal from 'ui/shared/LinkExternal';
import SolidityscanReportButton from 'ui/shared/solidityscanReport/SolidityscanReportButton';
import SolidityscanReportDetails from 'ui/shared/solidityscanReport/SolidityscanReportDetails';
import SolidityscanReportScore from 'ui/shared/solidityscanReport/SolidityscanReportScore';
type Props = {
id: string;
securityReport?: any; // eslint-disable-line @typescript-eslint/no-explicit-any
isLarge?: boolean;
showContractList: (id: string, type: ContractListTypes) => void;
}
const AppSecurityReport = ({ securityReport, isLarge }: Props) => {
const AppSecurityReport = ({ id, securityReport, isLarge, showContractList }: Props) => {
const {
overallInfo: {
securityScore,
......@@ -23,6 +26,11 @@ const AppSecurityReport = ({ securityReport, isLarge }: Props) => {
},
} = securityReport;
const showAnalyzedContracts = React.useCallback(() => {
showContractList(id, ContractListTypes.ANALYZED);
}, [ showContractList, id ]);
return (
<SolidityscanReportButton
height={ isLarge ? undefined : '30px' }
......@@ -40,7 +48,7 @@ const AppSecurityReport = ({ securityReport, isLarge }: Props) => {
<SolidityscanReportDetails vulnerabilities={ issueSeverityDistribution } vulnerabilitiesCount={ totalIssues }/>
</Box>
) }
<LinkExternal href="#">Analyzed contracts</LinkExternal>
<Link onClick={ showAnalyzedContracts }>Analyzed contracts</Link>
</>
) }
/>
......
......@@ -2,7 +2,7 @@ import { Flex, IconButton } from '@chakra-ui/react';
import React from 'react';
import type { MouseEvent } from 'react';
import type { MarketplaceAppPreview } from 'types/client/marketplace';
import type { MarketplaceAppPreview, ContractListTypes } from 'types/client/marketplace';
import * as mixpanel from 'lib/mixpanel/index';
import IconSvg from 'ui/shared/IconSvg';
......@@ -19,9 +19,10 @@ type Props = {
onFavoriteClick: (id: string, isFavorite: boolean) => void;
isLoading: boolean;
onAppClick: (event: MouseEvent, id: string) => void;
showContractList: (id: string, type: ContractListTypes) => void;
}
const ListItem = ({ app, onInfoClick, isFavorite, onFavoriteClick, isLoading, onAppClick }: Props) => {
const ListItem = ({ app, onInfoClick, isFavorite, onFavoriteClick, isLoading, onAppClick, showContractList }: Props) => {
const {
id,
securityReport,
......@@ -78,7 +79,7 @@ const ListItem = ({ app, onInfoClick, isFavorite, onFavoriteClick, isLoading, on
</Flex>
<Flex alignItems="center">
<Flex flex={ 1 } gap={ 3 } alignItems="center">
<AppSecurityReport securityReport={ securityReport }/>
<AppSecurityReport id={ id } securityReport={ securityReport } showContractList={ showContractList }/>
<LinkButton onClick={ handleInfoClick } icon="contracts">{ totalContractsNumber }</LinkButton>
<LinkButton onClick={ handleInfoClick } icon="contracts_verified" iconColor="green.500">{ verifiedNumber }</LinkButton>
</Flex>
......
......@@ -2,7 +2,7 @@ import { Table as ChakraTable, Tbody, Th, Tr } from '@chakra-ui/react';
import React from 'react';
import type { MouseEvent } from 'react';
import type { MarketplaceAppPreview } from 'types/client/marketplace';
import type { MarketplaceAppPreview, ContractListTypes } from 'types/client/marketplace';
import { default as Thead } from 'ui/shared/TheadSticky';
......@@ -15,9 +15,10 @@ type Props = {
onFavoriteClick: (id: string, isFavorite: boolean) => void;
onAppClick: (event: MouseEvent, id: string) => void;
onInfoClick: (id: string) => void;
showContractList: (id: string, type: ContractListTypes) => void;
}
const Table = ({ apps, isLoading, favoriteApps, onFavoriteClick, onAppClick, onInfoClick }: Props) => {
const Table = ({ apps, isLoading, favoriteApps, onFavoriteClick, onAppClick, onInfoClick, showContractList }: Props) => {
return (
<ChakraTable>
<Thead top={ 0 }>
......@@ -40,6 +41,7 @@ const Table = ({ apps, isLoading, favoriteApps, onFavoriteClick, onAppClick, onI
onFavoriteClick={ onFavoriteClick }
onAppClick={ onAppClick }
onInfoClick={ onInfoClick }
showContractList={ showContractList }
/>
)) }
</Tbody>
......
......@@ -3,6 +3,7 @@ import React from 'react';
import type { MouseEvent } from 'react';
import type { MarketplaceAppPreview } from 'types/client/marketplace';
import { ContractListTypes } from 'types/client/marketplace';
import * as mixpanel from 'lib/mixpanel/index';
import IconSvg from 'ui/shared/IconSvg';
......@@ -18,6 +19,7 @@ type Props = {
onFavoriteClick: (id: string, isFavorite: boolean) => void;
onAppClick: (event: MouseEvent, id: string) => void;
onInfoClick: (id: string) => void;
showContractList: (id: string, type: ContractListTypes) => void;
}
const TableItem = ({
......@@ -27,6 +29,7 @@ const TableItem = ({
onFavoriteClick,
onAppClick,
onInfoClick,
showContractList,
}: Props) => {
const {
......@@ -50,6 +53,14 @@ const TableItem = ({
onFavoriteClick(id, isFavorite);
}, [ onFavoriteClick, id, isFavorite ]);
const showAllContracts = React.useCallback(() => {
showContractList(id, ContractListTypes.ALL);
}, [ showContractList, id ]);
const showVerifiedContracts = React.useCallback(() => {
showContractList(id, ContractListTypes.VERIFIED);
}, [ showContractList, id ]);
return (
<Tr>
<Td verticalAlign="middle" px={ 2 }>
......@@ -71,13 +82,13 @@ const TableItem = ({
<AppLink app={ app } isLoading={ isLoading } onAppClick={ onAppClick } isLarge/>
</Td>
<Td verticalAlign="middle">
<AppSecurityReport securityReport={ securityReport } isLarge/>
<AppSecurityReport id={ id } securityReport={ securityReport } showContractList={ showContractList } isLarge/>
</Td>
<Td verticalAlign="middle">
<LinkButton onClick={ handleInfoClick } icon="contracts">{ totalContractsNumber }</LinkButton>
<LinkButton onClick={ showAllContracts } icon="contracts">{ totalContractsNumber }</LinkButton>
</Td>
<Td verticalAlign="middle">
<LinkButton onClick={ handleInfoClick } icon="contracts_verified" iconColor="green.500">{ verifiedNumber }</LinkButton>
<LinkButton onClick={ showVerifiedContracts } icon="contracts_verified" iconColor="green.500">{ verifiedNumber }</LinkButton>
</Td>
<Td verticalAlign="middle" isNumeric>
<LinkButton onClick={ handleInfoClick }>More info</LinkButton>
......
......@@ -2,6 +2,7 @@ import _pickBy from 'lodash/pickBy';
import { useRouter } from 'next/router';
import React from 'react';
import type { ContractListTypes } from 'types/client/marketplace';
import { MarketplaceCategory } from 'types/client/marketplace';
import useDebounce from 'lib/hooks/useDebounce';
......@@ -33,6 +34,7 @@ export default function useMarketplace() {
const [ isFavoriteAppsLoaded, setIsFavoriteAppsLoaded ] = React.useState<boolean>(false);
const [ isAppInfoModalOpen, setIsAppInfoModalOpen ] = React.useState<boolean>(false);
const [ isDisclaimerModalOpen, setIsDisclaimerModalOpen ] = React.useState<boolean>(false);
const [ contractListModalType, setContractListModalType ] = React.useState<ContractListTypes | null>(null);
const handleFavoriteClick = React.useCallback((id: string, isFavorite: boolean) => {
mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'Favorite app', Info: id });
......@@ -60,11 +62,17 @@ export default function useMarketplace() {
setIsDisclaimerModalOpen(true);
}, []);
const showContractList = React.useCallback((id: string, type: ContractListTypes) => {
setSelectedAppId(id);
setContractListModalType(type);
}, []);
const debouncedFilterQuery = useDebounce(filterQuery, 500);
const clearSelectedAppId = React.useCallback(() => {
setSelectedAppId(null);
setIsAppInfoModalOpen(false);
setIsDisclaimerModalOpen(false);
setContractListModalType(null);
}, []);
const handleCategoryChange = React.useCallback((newCategory: string) => {
......@@ -133,6 +141,8 @@ export default function useMarketplace() {
showDisclaimer,
appsTotal: data?.length || 0,
isCategoriesPlaceholderData,
showContractList,
contractListModalType,
}), [
selectedCategoryId,
categories,
......@@ -152,5 +162,7 @@ export default function useMarketplace() {
showDisclaimer,
data?.length,
isCategoriesPlaceholderData,
showContractList,
contractListModalType,
]);
}
......@@ -10,6 +10,7 @@ import { useAppContext } from 'lib/contexts/app';
import * as cookies from 'lib/cookies';
import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError';
import useIsMobile from 'lib/hooks/useIsMobile';
import ContractListModal from 'ui/marketplace/ContractListModal';
import MarketplaceAppModal from 'ui/marketplace/MarketplaceAppModal';
import MarketplaceDisclaimerModal from 'ui/marketplace/MarketplaceDisclaimerModal';
import MarketplaceList from 'ui/marketplace/MarketplaceList';
......@@ -68,6 +69,8 @@ const Marketplace = () => {
showDisclaimer,
appsTotal,
isCategoriesPlaceholderData,
showContractList,
contractListModalType,
} = useMarketplace();
const {
......@@ -134,6 +137,10 @@ const Marketplace = () => {
}
const selectedApp = displayedApps.find(app => app.id === selectedAppId);
const selectedAppContractList = securityReports
?.find(item => item.appName === selectedAppId)
?.chainsData[config.chain.name?.toLowerCase() || '']
?.contractsData;
return (
<>
......@@ -214,6 +221,7 @@ const Marketplace = () => {
selectedCategoryId={ selectedCategoryId }
onAppClick={ handleAppClick }
securityReports={ securityReports }
showContractList={ showContractList }
/>
) : (
<MarketplaceList
......@@ -243,6 +251,14 @@ const Marketplace = () => {
appId={ selectedApp.id }
/>
) }
{ (selectedApp && contractListModalType) && (
<ContractListModal
type={ contractListModalType }
contracts={ selectedAppContractList }
onClose={ clearSelectedAppId }
/>
) }
</>
);
};
......
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