Commit 37f66880 authored by Max Alekseenko's avatar Max Alekseenko

add security score component, integrate real data

parent 95a83474
...@@ -10,13 +10,19 @@ const submitFormUrl = getEnvValue('NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM'); ...@@ -10,13 +10,19 @@ const submitFormUrl = getEnvValue('NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM');
const suggestIdeasFormUrl = getEnvValue('NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM'); const suggestIdeasFormUrl = getEnvValue('NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM');
const categoriesUrl = getExternalAssetFilePath('NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL'); const categoriesUrl = getExternalAssetFilePath('NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL');
const adminServiceApiHost = getEnvValue('NEXT_PUBLIC_ADMIN_SERVICE_API_HOST'); const adminServiceApiHost = getEnvValue('NEXT_PUBLIC_ADMIN_SERVICE_API_HOST');
const securityReportsUrl = getExternalAssetFilePath('NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL');
const title = 'Marketplace'; const title = 'Marketplace';
const config: Feature<( const config: Feature<(
{ configUrl: string } | { configUrl: string } |
{ api: { endpoint: string; basePath: string } } { api: { endpoint: string; basePath: string } }
) & { submitFormUrl: string; categoriesUrl: string | undefined; suggestIdeasFormUrl: string | undefined } ) & {
submitFormUrl: string;
categoriesUrl: string | undefined;
suggestIdeasFormUrl: string | undefined;
securityReportsUrl: string | undefined;
}
> = (() => { > = (() => {
if (enabled === 'true' && chain.rpcUrl && submitFormUrl) { if (enabled === 'true' && chain.rpcUrl && submitFormUrl) {
if (configUrl) { if (configUrl) {
...@@ -27,6 +33,7 @@ const config: Feature<( ...@@ -27,6 +33,7 @@ const config: Feature<(
submitFormUrl, submitFormUrl,
categoriesUrl, categoriesUrl,
suggestIdeasFormUrl, suggestIdeasFormUrl,
securityReportsUrl,
}); });
} else if (adminServiceApiHost) { } else if (adminServiceApiHost) {
return Object.freeze({ return Object.freeze({
...@@ -35,6 +42,7 @@ const config: Feature<( ...@@ -35,6 +42,7 @@ const config: Feature<(
submitFormUrl, submitFormUrl,
categoriesUrl, categoriesUrl,
suggestIdeasFormUrl, suggestIdeasFormUrl,
securityReportsUrl,
api: { api: {
endpoint: adminServiceApiHost, endpoint: adminServiceApiHost,
basePath: '', basePath: '',
......
...@@ -16,6 +16,7 @@ ASSETS_DIR="$1" ...@@ -16,6 +16,7 @@ ASSETS_DIR="$1"
ASSETS_ENVS=( ASSETS_ENVS=(
"NEXT_PUBLIC_MARKETPLACE_CONFIG_URL" "NEXT_PUBLIC_MARKETPLACE_CONFIG_URL"
"NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL" "NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL"
"NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL"
"NEXT_PUBLIC_FEATURED_NETWORKS" "NEXT_PUBLIC_FEATURED_NETWORKS"
"NEXT_PUBLIC_FOOTER_LINKS" "NEXT_PUBLIC_FOOTER_LINKS"
"NEXT_PUBLIC_NETWORK_LOGO" "NEXT_PUBLIC_NETWORK_LOGO"
......
...@@ -150,8 +150,7 @@ const MarketplaceAppCard = ({ ...@@ -150,8 +150,7 @@ const MarketplaceAppCard = ({
{ !isLoading && ( { !isLoading && (
<IconButton <IconButton
display={{ base: 'block', sm: isFavorite ? 'block' : 'none' }} display="block"
_groupHover={{ display: 'block' }}
position="absolute" position="absolute"
right={{ base: 3, sm: '10px' }} right={{ base: 3, sm: '10px' }}
top={{ base: 3, sm: '14px' }} top={{ base: 3, sm: '14px' }}
...@@ -163,8 +162,8 @@ const MarketplaceAppCard = ({ ...@@ -163,8 +162,8 @@ const MarketplaceAppCard = ({
h={ 8 } h={ 8 }
onClick={ handleFavoriteClick } onClick={ handleFavoriteClick }
icon={ isFavorite ? icon={ isFavorite ?
<IconSvg name="star_filled" w={ 4 } h={ 4 } color="yellow.400"/> : <IconSvg name="star_filled" w={ 5 } h={ 5 } color="yellow.400"/> :
<IconSvg name="star_outline" w={ 4 } h={ 4 } color="gray.300"/> <IconSvg name="star_outline" w={ 5 } h={ 5 } color="gray.400"/>
} }
/> />
) } ) }
......
...@@ -138,8 +138,8 @@ const MarketplaceAppModal = ({ ...@@ -138,8 +138,8 @@ const MarketplaceAppModal = ({
h={ 8 } h={ 8 }
onClick={ handleFavoriteClick } onClick={ handleFavoriteClick }
icon={ isFavorite ? icon={ isFavorite ?
<IconSvg name="star_filled" w={ 4 } h={ 4 } color="yellow.400"/> : <IconSvg name="star_filled" w={ 5 } h={ 5 } color="yellow.400"/> :
<IconSvg name="star_outline" w={ 4 } h={ 4 } color="gray.300"/> } <IconSvg name="star_outline" w={ 5 } h={ 5 } color="gray.600"/> }
/> />
</Box> </Box>
</Box> </Box>
......
...@@ -5,6 +5,7 @@ import type { MouseEvent } from 'react'; ...@@ -5,6 +5,7 @@ import type { MouseEvent } from 'react';
import type { MarketplaceAppPreview } from 'types/client/marketplace'; import type { MarketplaceAppPreview } from 'types/client/marketplace';
import { MarketplaceCategory } from 'types/client/marketplace'; import { MarketplaceCategory } from 'types/client/marketplace';
import config from 'configs/app';
import { apos } from 'lib/html-entities'; import { apos } from 'lib/html-entities';
import DataListDisplay from 'ui/shared/DataListDisplay'; import DataListDisplay from 'ui/shared/DataListDisplay';
import EmptySearchResult from 'ui/shared/EmptySearchResult'; import EmptySearchResult from 'ui/shared/EmptySearchResult';
...@@ -21,13 +22,39 @@ interface Props { ...@@ -21,13 +22,39 @@ interface Props {
isLoading: boolean; isLoading: boolean;
selectedCategoryId?: string; selectedCategoryId?: string;
onAppClick: (event: MouseEvent, id: string) => void; onAppClick: (event: MouseEvent, id: string) => void;
securityReports: Array<any> | undefined; // eslint-disable-line @typescript-eslint/no-explicit-any
} }
const MarketplaceListWithScores = ({ apps, showAppInfo, favoriteApps, onFavoriteClick, isLoading, selectedCategoryId, onAppClick }: Props) => { const MarketplaceListWithScores = ({
apps,
showAppInfo,
favoriteApps,
onFavoriteClick,
isLoading,
selectedCategoryId,
onAppClick,
securityReports = [],
}: Props) => {
const displayedApps = React.useMemo(() =>
apps
.map((app) => {
const securityReport = securityReports.find(item => item.appName === app.id)?.chainsData[config.chain.name?.toLowerCase() || ''];
if (securityReport) {
const issues: Record<string, number> = securityReport.overallInfo.issueSeverityDistribution;
securityReport.overallInfo.totalIssues = Object.values(issues).reduce((acc, val) => acc + val, 0);
securityReport.overallInfo.securityScore = Number(securityReport.overallInfo.securityScore.toFixed(2));
}
return { ...app, securityReport };
})
.filter((app) => app.securityReport)
.sort((a, b) => b.securityReport.overallInfo.securityScore - a.securityReport.overallInfo.securityScore)
, [ apps, securityReports ]);
const content = apps.length > 0 ? ( const content = apps.length > 0 ? (
<> <>
<Show below="lg" ssr={ false }> <Show below="lg" ssr={ false }>
{ apps.map((app, index) => ( { displayedApps.map((app, index) => (
<ListItem <ListItem
key={ app.id + (isLoading ? index : '') } key={ app.id + (isLoading ? index : '') }
app={ app } app={ app }
...@@ -41,7 +68,7 @@ const MarketplaceListWithScores = ({ apps, showAppInfo, favoriteApps, onFavorite ...@@ -41,7 +68,7 @@ const MarketplaceListWithScores = ({ apps, showAppInfo, favoriteApps, onFavorite
</Show> </Show>
<Hide below="lg" ssr={ false }> <Hide below="lg" ssr={ false }>
<Table <Table
apps={ apps } apps={ displayedApps }
isLoading={ isLoading } isLoading={ isLoading }
onAppClick={ onAppClick } onAppClick={ onAppClick }
favoriteApps={ favoriteApps } favoriteApps={ favoriteApps }
......
import { Box, Text } from '@chakra-ui/react';
import React from 'react';
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 = {
securityReport?: any; // eslint-disable-line @typescript-eslint/no-explicit-any
isLarge?: boolean;
}
const AppSecurityReport = ({ securityReport, isLarge }: Props) => {
const {
overallInfo: {
securityScore,
solidityScanContractsNumber,
issueSeverityDistribution,
totalIssues,
},
} = securityReport;
return (
<SolidityscanReportButton
height={ isLarge ? undefined : '30px' }
score={ securityScore }
popoverContent={ (
<>
<Box mb={ 5 }>
{ solidityScanContractsNumber } smart contracts were evaluated to determine
this protocol{ apos }s overall security score 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="#">Analyzed contracts</LinkExternal>
</>
) }
/>
);
};
export default AppSecurityReport;
import { Flex, LinkBox, IconButton } from '@chakra-ui/react'; import { Flex, IconButton } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { MouseEvent } from 'react'; import type { MouseEvent } from 'react';
...@@ -9,10 +9,11 @@ import IconSvg from 'ui/shared/IconSvg'; ...@@ -9,10 +9,11 @@ import IconSvg from 'ui/shared/IconSvg';
import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile'; import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
import AppLink from './AppLink'; import AppLink from './AppLink';
import AppSecurityReport from './AppSecurityReport';
import LinkButton from './LinkButton'; import LinkButton from './LinkButton';
interface Props { type Props = {
app: MarketplaceAppPreview; app: MarketplaceAppPreview & { securityReport?: any }; // eslint-disable-line @typescript-eslint/no-explicit-any
onInfoClick: (id: string) => void; onInfoClick: (id: string) => void;
isFavorite: boolean; isFavorite: boolean;
onFavoriteClick: (id: string, isFavorite: boolean) => void; onFavoriteClick: (id: string, isFavorite: boolean) => void;
...@@ -21,7 +22,16 @@ interface Props { ...@@ -21,7 +22,16 @@ interface Props {
} }
const ListItem = ({ app, onInfoClick, isFavorite, onFavoriteClick, isLoading, onAppClick }: Props) => { const ListItem = ({ app, onInfoClick, isFavorite, onFavoriteClick, isLoading, onAppClick }: Props) => {
const { id } = app; const {
id,
securityReport,
securityReport: {
overallInfo: {
verifiedNumber,
totalContractsNumber,
},
},
} = app;
const handleInfoClick = React.useCallback((event: MouseEvent) => { const handleInfoClick = React.useCallback((event: MouseEvent) => {
event.preventDefault(); event.preventDefault();
...@@ -39,28 +49,19 @@ const ListItem = ({ app, onInfoClick, isFavorite, onFavoriteClick, isLoading, on ...@@ -39,28 +49,19 @@ const ListItem = ({ app, onInfoClick, isFavorite, onFavoriteClick, isLoading, on
py={ 3 } py={ 3 }
sx={{ ':first-child': { borderTop: 'none' }, ':last-child': { borderBottom: 'none' } }} sx={{ ':first-child': { borderTop: 'none' }, ':last-child': { borderBottom: 'none' } }}
> >
<LinkBox height="100%" width="100%" role="group"> <Flex
<Flex direction="column"
direction="column" justifyContent="stretch"
justifyContent="stretch" padding={ 3 }
padding={ 3 } width="100%"
> >
<Flex position="relative">
<AppLink app={ app } isLoading={ isLoading } onAppClick={ onAppClick }/> <AppLink app={ app } isLoading={ isLoading } onAppClick={ onAppClick }/>
<Flex>
<Flex flex={ 1 } gap={ 3 }>
<LinkButton onClick={ handleInfoClick } icon="contracts">13</LinkButton>
<LinkButton onClick={ handleInfoClick } icon="contracts_verified" iconColor="green.500">13</LinkButton>
</Flex>
{ !isLoading && (
<LinkButton onClick={ handleInfoClick }>More info</LinkButton>
) }
</Flex>
{ !isLoading && ( { !isLoading && (
<IconButton <IconButton
position="absolute" position="absolute"
right={ 0 } right={ -1 }
top={ 0 } top={ -1 }
aria-label="Mark as favorite" aria-label="Mark as favorite"
title="Mark as favorite" title="Mark as favorite"
variant="ghost" variant="ghost"
...@@ -69,13 +70,23 @@ const ListItem = ({ app, onInfoClick, isFavorite, onFavoriteClick, isLoading, on ...@@ -69,13 +70,23 @@ const ListItem = ({ app, onInfoClick, isFavorite, onFavoriteClick, isLoading, on
h={ 8 } h={ 8 }
onClick={ handleFavoriteClick } onClick={ handleFavoriteClick }
icon={ isFavorite ? icon={ isFavorite ?
<IconSvg name="star_filled" w={ 4 } h={ 4 } color="yellow.400"/> : <IconSvg name="star_filled" w={ 5 } h={ 5 } color="yellow.400"/> :
<IconSvg name="star_outline" w={ 4 } h={ 4 } color="gray.300"/> <IconSvg name="star_outline" w={ 5 } h={ 5 } color="gray.400"/>
} }
/> />
) } ) }
</Flex> </Flex>
</LinkBox> <Flex alignItems="center">
<Flex flex={ 1 } gap={ 3 } alignItems="center">
<AppSecurityReport securityReport={ securityReport }/>
<LinkButton onClick={ handleInfoClick } icon="contracts">{ totalContractsNumber }</LinkButton>
<LinkButton onClick={ handleInfoClick } icon="contracts_verified" iconColor="green.500">{ verifiedNumber }</LinkButton>
</Flex>
{ !isLoading && (
<LinkButton onClick={ handleInfoClick }>More info</LinkButton>
) }
</Flex>
</Flex>
</ListItemMobile> </ListItemMobile>
); );
}; };
......
...@@ -20,7 +20,7 @@ type Props = { ...@@ -20,7 +20,7 @@ type Props = {
const Table = ({ apps, isLoading, favoriteApps, onFavoriteClick, onAppClick, onInfoClick }: Props) => { const Table = ({ apps, isLoading, favoriteApps, onFavoriteClick, onAppClick, onInfoClick }: Props) => {
return ( return (
<ChakraTable> <ChakraTable>
<Thead top={ 80 }> <Thead top={ 0 }>
<Tr> <Tr>
<Th w="5%"></Th> <Th w="5%"></Th>
<Th w="40%">App</Th> <Th w="40%">App</Th>
......
...@@ -8,10 +8,11 @@ import * as mixpanel from 'lib/mixpanel/index'; ...@@ -8,10 +8,11 @@ import * as mixpanel from 'lib/mixpanel/index';
import IconSvg from 'ui/shared/IconSvg'; import IconSvg from 'ui/shared/IconSvg';
import AppLink from './AppLink'; import AppLink from './AppLink';
import AppSecurityReport from './AppSecurityReport';
import LinkButton from './LinkButton'; import LinkButton from './LinkButton';
type Props = { type Props = {
app: MarketplaceAppPreview; app: MarketplaceAppPreview & { securityReport?: any }; // eslint-disable-line @typescript-eslint/no-explicit-any
isLoading?: boolean; isLoading?: boolean;
isFavorite: boolean; isFavorite: boolean;
onFavoriteClick: (id: string, isFavorite: boolean) => void; onFavoriteClick: (id: string, isFavorite: boolean) => void;
...@@ -28,7 +29,16 @@ const TableItem = ({ ...@@ -28,7 +29,16 @@ const TableItem = ({
onInfoClick, onInfoClick,
}: Props) => { }: Props) => {
const { id } = app; const {
id,
securityReport,
securityReport: {
overallInfo: {
verifiedNumber,
totalContractsNumber,
},
},
} = app;
const handleInfoClick = React.useCallback((event: MouseEvent) => { const handleInfoClick = React.useCallback((event: MouseEvent) => {
event.preventDefault(); event.preventDefault();
...@@ -52,20 +62,22 @@ const TableItem = ({ ...@@ -52,20 +62,22 @@ const TableItem = ({
h={ 8 } h={ 8 }
onClick={ handleFavoriteClick } onClick={ handleFavoriteClick }
icon={ isFavorite ? icon={ isFavorite ?
<IconSvg name="star_filled" w={ 4 } h={ 4 } color="yellow.400"/> : <IconSvg name="star_filled" w={ 5 } h={ 5 } color="yellow.400"/> :
<IconSvg name="star_outline" w={ 4 } h={ 4 } color="gray.300"/> <IconSvg name="star_outline" w={ 5 } h={ 5 } color="gray.400"/>
} }
/> />
</Td> </Td>
<Td verticalAlign="middle"> <Td verticalAlign="middle">
<AppLink app={ app } isLoading={ isLoading } onAppClick={ onAppClick } isLarge/> <AppLink app={ app } isLoading={ isLoading } onAppClick={ onAppClick } isLarge/>
</Td> </Td>
<Td verticalAlign="middle"></Td>
<Td verticalAlign="middle"> <Td verticalAlign="middle">
<LinkButton onClick={ handleInfoClick } icon="contracts">13</LinkButton> <AppSecurityReport securityReport={ securityReport } isLarge/>
</Td> </Td>
<Td verticalAlign="middle"> <Td verticalAlign="middle">
<LinkButton onClick={ handleInfoClick } icon="contracts_verified" iconColor="green.500">13</LinkButton> <LinkButton onClick={ handleInfoClick } icon="contracts">{ totalContractsNumber }</LinkButton>
</Td>
<Td verticalAlign="middle">
<LinkButton onClick={ handleInfoClick } icon="contracts_verified" iconColor="green.500">{ verifiedNumber }</LinkButton>
</Td> </Td>
<Td verticalAlign="middle" isNumeric> <Td verticalAlign="middle" isNumeric>
<LinkButton onClick={ handleInfoClick }>More info</LinkButton> <LinkButton onClick={ handleInfoClick }>More info</LinkButton>
......
import { useQuery } from '@tanstack/react-query';
import config from 'configs/app';
import type { ResourceError } from 'lib/api/resources';
import useApiFetch from 'lib/hooks/useFetch';
const feature = config.features.marketplace;
const securityReportsUrl = (feature.isEnabled && feature.securityReportsUrl) || '';
export default function useSecurityReports() {
const apiFetch = useApiFetch();
return useQuery<unknown, ResourceError<unknown>, Array<any>>({ // eslint-disable-line @typescript-eslint/no-explicit-any
queryKey: [ 'marketplace-security-reports' ],
queryFn: async() => apiFetch(securityReportsUrl, undefined, { resource: 'marketplace-security-reports' }),
placeholderData: securityReportsUrl ? [] : undefined,
staleTime: Infinity,
enabled: Boolean(securityReportsUrl),
});
}
...@@ -24,6 +24,7 @@ import TabsSkeleton from 'ui/shared/Tabs/TabsSkeleton'; ...@@ -24,6 +24,7 @@ import TabsSkeleton from 'ui/shared/Tabs/TabsSkeleton';
import TabsWithScroll from 'ui/shared/Tabs/TabsWithScroll'; import TabsWithScroll from 'ui/shared/Tabs/TabsWithScroll';
import useMarketplace from '../marketplace/useMarketplace'; import useMarketplace from '../marketplace/useMarketplace';
import useSecurityReports from '../marketplace/useSecurityReports';
const feature = config.features.marketplace; const feature = config.features.marketplace;
const links: Array<{ label: string; href: string; icon: IconName }> = []; const links: Array<{ label: string; href: string; icon: IconName }> = [];
...@@ -68,6 +69,12 @@ const Marketplace = () => { ...@@ -68,6 +69,12 @@ const Marketplace = () => {
appsTotal, appsTotal,
isCategoriesPlaceholderData, isCategoriesPlaceholderData,
} = useMarketplace(); } = useMarketplace();
const {
data: securityReports,
isPlaceholderData: isSecurityReportsPlaceholderData,
} = useSecurityReports();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const categoryTabs = React.useMemo(() => { const categoryTabs = React.useMemo(() => {
...@@ -203,9 +210,10 @@ const Marketplace = () => { ...@@ -203,9 +210,10 @@ const Marketplace = () => {
showAppInfo={ showAppInfo } showAppInfo={ showAppInfo }
favoriteApps={ favoriteApps } favoriteApps={ favoriteApps }
onFavoriteClick={ onFavoriteClick } onFavoriteClick={ onFavoriteClick }
isLoading={ isPlaceholderData } isLoading={ isPlaceholderData || isSecurityReportsPlaceholderData }
selectedCategoryId={ selectedCategoryId } selectedCategoryId={ selectedCategoryId }
onAppClick={ handleAppClick } onAppClick={ handleAppClick }
securityReports={ securityReports }
/> />
) : ( ) : (
<MarketplaceList <MarketplaceList
......
...@@ -19,9 +19,10 @@ interface Props { ...@@ -19,9 +19,10 @@ interface Props {
score: number; score: number;
popoverContent?: React.ReactNode; popoverContent?: React.ReactNode;
isLoading?: boolean; isLoading?: boolean;
height?: string;
} }
const SolidityscanReportButton = ({ className, score, popoverContent, isLoading }: Props) => { const SolidityscanReportButton = ({ className, score, popoverContent, isLoading, height = '32px' }: Props) => {
const { isOpen, onToggle, onClose } = useDisclosure(); const { isOpen, onToggle, onClose } = useDisclosure();
const { scoreColor } = useScoreLevelAndColor(score); const { scoreColor } = useScoreLevelAndColor(score);
...@@ -39,7 +40,7 @@ const SolidityscanReportButton = ({ className, score, popoverContent, isLoading ...@@ -39,7 +40,7 @@ const SolidityscanReportButton = ({ className, score, popoverContent, isLoading
aria-label="SolidityScan score" aria-label="SolidityScan score"
fontWeight={ 500 } fontWeight={ 500 }
px="6px" px="6px"
h="32px" h={ height }
flexShrink={ 0 } flexShrink={ 0 }
> >
<IconSvg name={ score < 80 ? 'score/score-not-ok' : 'score/score-ok' } boxSize={ 5 } mr={ 1 }/> <IconSvg name={ score < 80 ? 'score/score-not-ok' : 'score/score-ok' } boxSize={ 5 } mr={ 1 }/>
......
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