Commit 4fde5d21 authored by Max Alekseenko's avatar Max Alekseenko Committed by GitHub

Merge pull request #1709 from blockscout/dapps-security-score

Dapps security score
parents 586cfbce 946f77de
...@@ -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: '',
......
...@@ -48,6 +48,7 @@ NEXT_PUBLIC_LOGOUT_URL=https://blockscoutcom.us.auth0.com/v2/logout ...@@ -48,6 +48,7 @@ NEXT_PUBLIC_LOGOUT_URL=https://blockscoutcom.us.auth0.com/v2/logout
NEXT_PUBLIC_MARKETPLACE_ENABLED=true NEXT_PUBLIC_MARKETPLACE_ENABLED=true
NEXT_PUBLIC_MARKETPLACE_CONFIG_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/marketplace/eth-goerli.json NEXT_PUBLIC_MARKETPLACE_CONFIG_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/marketplace/eth-goerli.json
NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/marketplace-categories/default.json NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/marketplace-categories/default.json
NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL=https://gist.githubusercontent.com/maxaleks/ce5c7e3de53e8f5b240b88265daf5839/raw/328383c958a8f7ecccf6d50c953bcdf8ab3faa0a/security_reports_goerli_test.json
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/shrqUAcjgGJ4jU88C NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/shrqUAcjgGJ4jU88C
NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM=https://airtable.com/appiy5yijZpMMSKjT/pag3t82DUCyhGRZZO/form NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM=https://airtable.com/appiy5yijZpMMSKjT/pag3t82DUCyhGRZZO/form
NEXT_PUBLIC_STATS_API_HOST=https://stats-goerli.k8s-dev.blockscout.com NEXT_PUBLIC_STATS_API_HOST=https://stats-goerli.k8s-dev.blockscout.com
......
...@@ -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"
......
...@@ -37,6 +37,7 @@ async function validateEnvs(appEnvs: Record<string, string>) { ...@@ -37,6 +37,7 @@ async function validateEnvs(appEnvs: Record<string, string>) {
'NEXT_PUBLIC_FEATURED_NETWORKS', 'NEXT_PUBLIC_FEATURED_NETWORKS',
'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_FOOTER_LINKS', 'NEXT_PUBLIC_FOOTER_LINKS',
]; ];
......
...@@ -14,7 +14,7 @@ import type { AdTextProviders, AdBannerProviders, AdBannerAdditionalProviders } ...@@ -14,7 +14,7 @@ import type { AdTextProviders, AdBannerProviders, AdBannerAdditionalProviders }
import type { ContractCodeIde } from '../../../types/client/contract'; import type { ContractCodeIde } from '../../../types/client/contract';
import { GAS_UNITS } from '../../../types/client/gasTracker'; import { GAS_UNITS } from '../../../types/client/gasTracker';
import type { GasUnit } from '../../../types/client/gasTracker'; import type { GasUnit } from '../../../types/client/gasTracker';
import type { MarketplaceAppOverview } from '../../../types/client/marketplace'; import type { MarketplaceAppOverview, MarketplaceAppSecurityReportRaw, MarketplaceAppSecurityReport } from '../../../types/client/marketplace';
import { NAVIGATION_LINK_IDS } from '../../../types/client/navigation-items'; import { NAVIGATION_LINK_IDS } from '../../../types/client/navigation-items';
import type { NavItemExternal, NavigationLinkId } from '../../../types/client/navigation-items'; import type { NavItemExternal, NavigationLinkId } from '../../../types/client/navigation-items';
import { ROLLUP_TYPES } from '../../../types/client/rollup'; import { ROLLUP_TYPES } from '../../../types/client/rollup';
...@@ -85,6 +85,65 @@ const marketplaceAppSchema: yup.ObjectSchema<MarketplaceAppOverview> = yup ...@@ -85,6 +85,65 @@ const marketplaceAppSchema: yup.ObjectSchema<MarketplaceAppOverview> = yup
priority: yup.number(), priority: yup.number(),
}); });
const issueSeverityDistributionSchema: yup.ObjectSchema<MarketplaceAppSecurityReport['overallInfo']['issueSeverityDistribution']> = yup
.object({
critical: yup.number().required(),
gas: yup.number().required(),
high: yup.number().required(),
informational: yup.number().required(),
low: yup.number().required(),
medium: yup.number().required(),
});
const solidityscanReportSchema: yup.ObjectSchema<MarketplaceAppSecurityReport['contractsData'][number]['solidityScanReport']> = yup
.object({
contractname: yup.string().required(),
scan_status: yup.string().required(),
scan_summary: yup
.object({
issue_severity_distribution: issueSeverityDistributionSchema.required(),
lines_analyzed_count: yup.number().required(),
scan_time_taken: yup.number().required(),
score: yup.string().required(),
score_v2: yup.string().required(),
threat_score: yup.string().required(),
})
.required(),
scanner_reference_url: yup.string().test(urlTest).required(),
});
const contractDataSchema: yup.ObjectSchema<MarketplaceAppSecurityReport['contractsData'][number]> = yup
.object({
address: yup.string().required(),
isVerified: yup.boolean().required(),
solidityScanReport: solidityscanReportSchema.nullable().notRequired(),
});
const chainsDataSchema = yup.lazy((objValue) => {
let schema = yup.object();
Object.keys(objValue).forEach((key) => {
schema = schema.shape({
[key]: yup.object({
overallInfo: yup.object({
verifiedNumber: yup.number().required(),
totalContractsNumber: yup.number().required(),
solidityScanContractsNumber: yup.number().required(),
securityScore: yup.number().required(),
issueSeverityDistribution: issueSeverityDistributionSchema.required(),
}).required(),
contractsData: yup.array().of(contractDataSchema).required(),
}),
});
});
return schema;
});
const securityReportSchema: yup.ObjectSchema<MarketplaceAppSecurityReportRaw> = yup
.object({
appName: yup.string().required(),
chainsData: chainsDataSchema,
});
const marketplaceSchema = yup const marketplaceSchema = yup
.object() .object()
.shape({ .shape({
...@@ -125,6 +184,16 @@ const marketplaceSchema = yup ...@@ -125,6 +184,16 @@ const marketplaceSchema = yup
// eslint-disable-next-line max-len // eslint-disable-next-line max-len
otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM cannot not be used without NEXT_PUBLIC_MARKETPLACE_ENABLED'), otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM cannot not be used without NEXT_PUBLIC_MARKETPLACE_ENABLED'),
}), }),
NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL: yup
.array()
.json()
.of(securityReportSchema)
.when('NEXT_PUBLIC_MARKETPLACE_ENABLED', {
is: true,
then: (schema) => schema,
// eslint-disable-next-line max-len
otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL cannot not be used without NEXT_PUBLIC_MARKETPLACE_ENABLED'),
}),
}); });
const beaconChainSchema = yup const beaconChainSchema = yup
......
...@@ -3,4 +3,5 @@ NEXT_PUBLIC_MARKETPLACE_CONFIG_URL=https://example.com ...@@ -3,4 +3,5 @@ NEXT_PUBLIC_MARKETPLACE_CONFIG_URL=https://example.com
NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL=https://example.com NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL=https://example.com
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://example.com NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://example.com
NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM=https://example.com NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM=https://example.com
NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL=https://example.com
NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://example.com NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://example.com
[
{
"appName": "paraswap",
"doc": "https://developers.paraswap.network/smart-contracts",
"chainsData": {
"1": {
"overallInfo": {
"verifiedNumber": 4,
"totalContractsNumber": 4,
"solidityScanContractsNumber": 4,
"securityScore": 77.41749999999999,
"issueSeverityDistribution": {
"critical": 5,
"gas": 58,
"high": 9,
"informational": 27,
"low": 41,
"medium": 5
}
},
"contractsData": [
{
"address": "0xDEF171Fe48CF0115B1d80b88dc8eAB59176FEe57",
"isVerified": true,
"solidityScanReport": {
"connection_id": "",
"contract_address": "0xDEF171Fe48CF0115B1d80b88dc8eAB59176FEe57",
"contract_chain": "optimism",
"contract_platform": "blockscout",
"contract_url": "https://optimism.blockscout.com/address/0xDEF171Fe48CF0115B1d80b88dc8eAB59176FEe57",
"contractname": "AugustusSwapper",
"is_quick_scan": true,
"node_reference_id": null,
"request_type": "threat_scan",
"scanner_reference_url": "https://solidityscan.com/quickscan/0xDEF171Fe48CF0115B1d80b88dc8eAB59176FEe57/blockscout/eth?ref=blockscout",
"scan_status": "scan_done",
"scan_summary": {
"issue_severity_distribution": {
"critical": 0,
"gas": 8,
"high": 4,
"informational": 7,
"low": 8,
"medium": 1
},
"lines_analyzed_count": 180,
"scan_time_taken": 1,
"score": "3.61",
"score_v2": "72.22",
"threat_score": "73.68"
}
}
},
{
"address": "0x216b4b4ba9f3e719726886d34a177484278bfcae",
"isVerified": true,
"solidityScanReport": {
"connection_id": "",
"contract_address": "0x216b4b4ba9f3e719726886d34a177484278bfcae",
"contract_chain": "eth",
"contract_platform": "blockscout",
"contract_url": "https://eth.blockscout.com/address/0x216b4b4ba9f3e719726886d34a177484278bfcae",
"contractname": "TokenTransferProxy",
"is_quick_scan": true,
"node_reference_id": null,
"request_type": "threat_scan",
"scanner_reference_url": "https://solidityscan.com/quickscan/0x216b4b4ba9f3e719726886d34a177484278bfcae/blockscout/eth?ref=blockscout",
"scan_status": "scan_done",
"scan_summary": {
"issue_severity_distribution": {
"critical": 1,
"gas": 29,
"high": 5,
"informational": 14,
"low": 21,
"medium": 3
},
"lines_analyzed_count": 553,
"scan_time_taken": 1,
"score": "3.92",
"score_v2": "78.48",
"threat_score": "78.95"
}
}
},
{
"address": "0xa68bEA62Dc4034A689AA0F58A76681433caCa663",
"isVerified": true,
"solidityScanReport": {
"connection_id": "",
"contract_address": "0xa68bEA62Dc4034A689AA0F58A76681433caCa663",
"contract_chain": "eth",
"contract_platform": "blockscout",
"contract_url": "https://eth.blockscout.com/address/0xa68bEA62Dc4034A689AA0F58A76681433caCa663",
"contractname": "AugustusRegistry",
"is_quick_scan": true,
"node_reference_id": null,
"request_type": "threat_scan",
"scanner_reference_url": "https://solidityscan.com/quickscan/0xa68bEA62Dc4034A689AA0F58A76681433caCa663/blockscout/eth?ref=blockscout",
"scan_status": "scan_done",
"scan_summary": {
"issue_severity_distribution": {
"critical": 0,
"gas": 3,
"high": 0,
"informational": 5,
"low": 4,
"medium": 0
},
"lines_analyzed_count": 103,
"scan_time_taken": 0,
"score": "4.22",
"score_v2": "84.47",
"threat_score": "88.89"
}
}
},
{
"address": "0xeF13101C5bbD737cFb2bF00Bbd38c626AD6952F7",
"isVerified": true,
"solidityScanReport": {
"connection_id": "",
"contract_address": "0xeF13101C5bbD737cFb2bF00Bbd38c626AD6952F7",
"contract_chain": "eth",
"contract_platform": "blockscout",
"contract_url": "https://eth.blockscout.com/address/0xeF13101C5bbD737cFb2bF00Bbd38c626AD6952F7",
"contractname": "FeeClaimer",
"is_quick_scan": true,
"node_reference_id": null,
"request_type": "threat_scan",
"scanner_reference_url": "https://solidityscan.com/quickscan/0xeF13101C5bbD737cFb2bF00Bbd38c626AD6952F7/blockscout/eth?ref=blockscout",
"scan_status": "scan_done",
"scan_summary": {
"issue_severity_distribution": {
"critical": 0,
"gas": 18,
"high": 0,
"informational": 1,
"low": 8,
"medium": 1
},
"lines_analyzed_count": 149,
"scan_time_taken": 0,
"score": "3.72",
"score_v2": "74.50",
"threat_score": "94.74"
}
}
}
]
},
"10": {
"overallInfo": {
"verifiedNumber": 3,
"totalContractsNumber": 4,
"solidityScanContractsNumber": 3,
"securityScore": 75.44333333333333,
"issueSeverityDistribution": {
"critical": 4,
"gas": 29,
"high": 4,
"informational": 20,
"low": 20,
"medium": 2
}
},
"contractsData": [
{
"address": "0xDEF171Fe48CF0115B1d80b88dc8eAB59176FEe57",
"isVerified": true,
"solidityScanReport": {
"connection_id": "",
"contract_address": "0xDEF171Fe48CF0115B1d80b88dc8eAB59176FEe57",
"contract_chain": "optimism",
"contract_platform": "blockscout",
"contract_url": "https://optimism.blockscout.com/address/0xDEF171Fe48CF0115B1d80b88dc8eAB59176FEe57",
"contractname": "AugustusSwapper",
"is_quick_scan": true,
"node_reference_id": null,
"request_type": "threat_scan",
"scanner_reference_url": "https://solidityscan.com/quickscan/0xDEF171Fe48CF0115B1d80b88dc8eAB59176FEe57/blockscout/optimism?ref=blockscout",
"scan_status": "scan_done",
"scan_summary": {
"issue_severity_distribution": {
"critical": 0,
"gas": 8,
"high": 4,
"informational": 7,
"low": 8,
"medium": 1
},
"lines_analyzed_count": 180,
"scan_time_taken": 1,
"score": "3.61",
"score_v2": "72.22",
"threat_score": "73.68"
}
}
},
{
"address": "0x216B4B4Ba9F3e719726886d34a177484278Bfcae",
"isVerified": false,
"solidityScanReport": null
},
{
"address": "0x6e7bE86000dF697facF4396efD2aE2C322165dC3",
"isVerified": true,
"solidityScanReport": {
"connection_id": "",
"contract_address": "0x6e7bE86000dF697facF4396efD2aE2C322165dC3",
"contract_chain": "optimism",
"contract_platform": "blockscout",
"contract_url": "https://optimism.blockscout.com/address/0x6e7bE86000dF697facF4396efD2aE2C322165dC3",
"contractname": "AugustusRegistry",
"is_quick_scan": true,
"node_reference_id": null,
"request_type": "threat_scan",
"scanner_reference_url": "https://solidityscan.com/quickscan/0x6e7bE86000dF697facF4396efD2aE2C322165dC3/blockscout/optimism?ref=blockscout",
"scan_status": "scan_done",
"scan_summary": {
"issue_severity_distribution": {
"critical": 0,
"gas": 3,
"high": 0,
"informational": 5,
"low": 4,
"medium": 0
},
"lines_analyzed_count": 102,
"scan_time_taken": 0,
"score": "4.22",
"score_v2": "84.31",
"threat_score": "88.89"
}
}
},
{
"address": "0xA7465CCD97899edcf11C56D2d26B49125674e45F",
"isVerified": true,
"solidityScanReport": {
"connection_id": "",
"contract_address": "0xA7465CCD97899edcf11C56D2d26B49125674e45F",
"contract_chain": "optimism",
"contract_platform": "blockscout",
"contract_url": "https://optimism.blockscout.com/address/0xA7465CCD97899edcf11C56D2d26B49125674e45F",
"contractname": "FeeClaimer",
"is_quick_scan": true,
"node_reference_id": null,
"request_type": "threat_scan",
"scanner_reference_url": "https://solidityscan.com/quickscan/0xA7465CCD97899edcf11C56D2d26B49125674e45F/blockscout/optimism?ref=blockscout",
"scan_status": "scan_done",
"scan_summary": {
"issue_severity_distribution": {
"critical": 0,
"gas": 18,
"high": 0,
"informational": 8,
"low": 8,
"medium": 1
},
"lines_analyzed_count": 149,
"scan_time_taken": 1,
"score": "3.49",
"score_v2": "69.80",
"threat_score": "94.74"
}
}
}
]
},
"8453": {
"overallInfo": {
"verifiedNumber": 1,
"totalContractsNumber": 4,
"solidityScanContractsNumber": 1,
"securityScore": 73.33,
"issueSeverityDistribution": {
"critical": 4,
"gas": 8,
"high": 4,
"informational": 5,
"low": 8,
"medium": 1
}
},
"contractsData": [
{
"address": "0x59C7C832e96D2568bea6db468C1aAdcbbDa08A52",
"isVerified": true,
"solidityScanReport": {
"connection_id": "",
"contract_address": "0x59C7C832e96D2568bea6db468C1aAdcbbDa08A52",
"contract_chain": "base",
"contract_platform": "blockscout",
"contract_url": "https://base.blockscout.com/address/0x59C7C832e96D2568bea6db468C1aAdcbbDa08A52",
"contractname": "AugustusSwapper",
"is_quick_scan": true,
"node_reference_id": null,
"request_type": "threat_scan",
"scanner_reference_url": "https://solidityscan.com/quickscan/0x59C7C832e96D2568bea6db468C1aAdcbbDa08A52/blockscout/base?ref=blockscout",
"scan_status": "scan_done",
"scan_summary": {
"issue_severity_distribution": {
"critical": 0,
"gas": 8,
"high": 4,
"informational": 5,
"low": 8,
"medium": 1
},
"lines_analyzed_count": 180,
"scan_time_taken": 1,
"score": "3.67",
"score_v2": "73.33",
"threat_score": "73.68"
}
}
},
{
"address": "0x93aAAe79a53759cD164340E4C8766E4Db5331cD7",
"isVerified": false,
"solidityScanReport": null
},
{
"address": "0x7e31b336f9e8ba52ba3c4ac861b033ba90900bb3",
"isVerified": false,
"solidityScanReport": null
},
{
"address": "0x9aaB4B24541af30fD72784ED98D8756ac0eFb3C7",
"isVerified": false,
"solidityScanReport": null
}
]
}
}
},
{
"appName": "mean-finance",
"doc": "https://docs.mean.finance/guides/smart-contract-registry",
"chainsData": {
"1": {
"overallInfo": {
"verifiedNumber": 4,
"totalContractsNumber": 6,
"solidityScanContractsNumber": 4,
"securityScore": 61.36750000000001,
"issueSeverityDistribution": {
"critical": 6,
"gas": 25,
"high": 1,
"informational": 10,
"low": 20,
"medium": 3
}
},
"contractsData": [
{
"address": "0xA5AdC5484f9997fBF7D405b9AA62A7d88883C345",
"isVerified": false,
"solidityScanReport": null
},
{
"address": "0x20bdAE1413659f47416f769a4B27044946bc9923",
"isVerified": true,
"solidityScanReport": {
"connection_id": "",
"contract_address": "0x20bdAE1413659f47416f769a4B27044946bc9923",
"contract_chain": "optimism",
"contract_platform": "blockscout",
"contract_url": "https://optimism.blockscout.com/address/0x20bdAE1413659f47416f769a4B27044946bc9923",
"contractname": "DCAPermissionsManager",
"is_quick_scan": true,
"node_reference_id": null,
"request_type": "threat_scan",
"scanner_reference_url": "https://solidityscan.com/quickscan/0x20bdAE1413659f47416f769a4B27044946bc9923/blockscout/eth?ref=blockscout",
"scan_status": "scan_done",
"scan_summary": {
"issue_severity_distribution": {
"critical": 2,
"gas": 22,
"high": 0,
"informational": 8,
"low": 11,
"medium": 3
},
"lines_analyzed_count": 314,
"scan_time_taken": 1,
"score": "3.87",
"score_v2": "77.39",
"threat_score": "88.89"
}
}
},
{
"address": "0xDf0dbc66f85979a1d54671c4D9e439F306Be27EE",
"isVerified": false,
"solidityScanReport": null
},
{
"address": "0x4ACd4BC402bc8e6BA8aBDdcA639d8011ef0b8a4b",
"isVerified": true,
"solidityScanReport": {
"connection_id": "",
"contract_address": "0x4ACd4BC402bc8e6BA8aBDdcA639d8011ef0b8a4b",
"contract_chain": "eth",
"contract_platform": "blockscout",
"contract_url": "https://eth.blockscout.com/address/0x4ACd4BC402bc8e6BA8aBDdcA639d8011ef0b8a4b",
"contractname": "DCAHubPositionDescriptor",
"is_quick_scan": true,
"node_reference_id": null,
"request_type": "threat_scan",
"scanner_reference_url": "https://solidityscan.com/quickscan/0x4ACd4BC402bc8e6BA8aBDdcA639d8011ef0b8a4b/blockscout/eth?ref=blockscout",
"scan_status": "scan_done",
"scan_summary": {
"issue_severity_distribution": {
"critical": 0,
"gas": 1,
"high": 1,
"informational": 2,
"low": 3,
"medium": 0
},
"lines_analyzed_count": 280,
"scan_time_taken": 1,
"score": "4.77",
"score_v2": "95.36",
"threat_score": "100.00"
}
}
},
{
"address": "0x49c590F6a2dfB0f809E82B9e2BF788C0Dd1c31f9",
"isVerified": true,
"solidityScanReport": {
"connection_id": "",
"contract_address": "0x49c590F6a2dfB0f809E82B9e2BF788C0Dd1c31f9",
"contract_chain": "eth",
"contract_platform": "blockscout",
"contract_url": "https://eth.blockscout.com/address/0x49c590F6a2dfB0f809E82B9e2BF788C0Dd1c31f9",
"contractname": "DCAHubCompanion",
"is_quick_scan": true,
"node_reference_id": null,
"request_type": "threat_scan",
"scanner_reference_url": "https://solidityscan.com/quickscan/0x49c590F6a2dfB0f809E82B9e2BF788C0Dd1c31f9/blockscout/eth?ref=blockscout",
"scan_status": "scan_done",
"scan_summary": {
"issue_severity_distribution": {
"critical": 0,
"gas": 1,
"high": 0,
"informational": 0,
"low": 3,
"medium": 0
},
"lines_analyzed_count": 11,
"scan_time_taken": 0,
"score": "1.82",
"score_v2": "36.36",
"threat_score": "100.00"
}
}
},
{
"address": "0x5ad2fED59E8DF461c6164c31B4267Efb7cBaF9C0",
"isVerified": true,
"solidityScanReport": {
"connection_id": "",
"contract_address": "0x5ad2fED59E8DF461c6164c31B4267Efb7cBaF9C0",
"contract_chain": "eth",
"contract_platform": "blockscout",
"contract_url": "https://eth.blockscout.com/address/0x5ad2fED59E8DF461c6164c31B4267Efb7cBaF9C0",
"contractname": "DCAHubCompanion",
"is_quick_scan": true,
"node_reference_id": null,
"request_type": "threat_scan",
"scanner_reference_url": "https://solidityscan.com/quickscan/0x5ad2fED59E8DF461c6164c31B4267Efb7cBaF9C0/blockscout/eth?ref=blockscout",
"scan_status": "scan_done",
"scan_summary": {
"issue_severity_distribution": {
"critical": 0,
"gas": 1,
"high": 0,
"informational": 0,
"low": 3,
"medium": 0
},
"lines_analyzed_count": 11,
"scan_time_taken": 0,
"score": "1.82",
"score_v2": "36.36",
"threat_score": "100.00"
}
}
}
]
},
"10": {
"overallInfo": {
"verifiedNumber": 5,
"totalContractsNumber": 6,
"solidityScanContractsNumber": 5,
"securityScore": 66.986,
"issueSeverityDistribution": {
"critical": 6,
"gas": 26,
"high": 1,
"informational": 10,
"low": 23,
"medium": 3
}
},
"contractsData": [
{
"address": "0xA5AdC5484f9997fBF7D405b9AA62A7d88883C345",
"isVerified": true,
"solidityScanReport": {
"connection_id": "",
"contract_address": "0xA5AdC5484f9997fBF7D405b9AA62A7d88883C345",
"contract_chain": "optimism",
"contract_platform": "blockscout",
"contract_url": "https://optimism.blockscout.com/address/0xA5AdC5484f9997fBF7D405b9AA62A7d88883C345",
"contractname": "DCAHub",
"is_quick_scan": true,
"node_reference_id": null,
"request_type": "threat_scan",
"scanner_reference_url": "https://solidityscan.com/quickscan/0xA5AdC5484f9997fBF7D405b9AA62A7d88883C345/blockscout/optimism?ref=blockscout",
"scan_status": "scan_done",
"scan_summary": {
"issue_severity_distribution": {
"critical": 0,
"gas": 1,
"high": 0,
"informational": 0,
"low": 3,
"medium": 0
},
"lines_analyzed_count": 23,
"scan_time_taken": 0,
"score": "3.48",
"score_v2": "69.57",
"threat_score": "94.44"
}
}
},
{
"address": "0x20bdAE1413659f47416f769a4B27044946bc9923",
"isVerified": true,
"solidityScanReport": {
"connection_id": "",
"contract_address": "0x20bdAE1413659f47416f769a4B27044946bc9923",
"contract_chain": "optimism",
"contract_platform": "blockscout",
"contract_url": "https://optimism.blockscout.com/address/0x20bdAE1413659f47416f769a4B27044946bc9923",
"contractname": "DCAPermissionsManager",
"is_quick_scan": true,
"node_reference_id": null,
"request_type": "threat_scan",
"scanner_reference_url": "https://solidityscan.com/quickscan/0x20bdAE1413659f47416f769a4B27044946bc9923/blockscout/optimism?ref=blockscout",
"scan_status": "scan_done",
"scan_summary": {
"issue_severity_distribution": {
"critical": 2,
"gas": 22,
"high": 0,
"informational": 8,
"low": 11,
"medium": 3
},
"lines_analyzed_count": 314,
"scan_time_taken": 1,
"score": "3.87",
"score_v2": "77.39",
"threat_score": "88.89"
}
}
},
{
"address": "0xDf0dbc66f85979a1d54671c4D9e439F306Be27EE",
"isVerified": true,
"solidityScanReport": {
"connection_id": "",
"contract_address": "0xDf0dbc66f85979a1d54671c4D9e439F306Be27EE",
"contract_chain": "optimism",
"contract_platform": "blockscout",
"contract_url": "https://optimism.blockscout.com/address/0xDf0dbc66f85979a1d54671c4D9e439F306Be27EE",
"contractname": "DCAHubCompanion",
"is_quick_scan": true,
"node_reference_id": null,
"request_type": "threat_scan",
"scanner_reference_url": "https://solidityscan.com/quickscan/0xDf0dbc66f85979a1d54671c4D9e439F306Be27EE/blockscout/optimism?ref=blockscout",
"scan_status": "scan_done",
"scan_summary": {
"issue_severity_distribution": {
"critical": 0,
"gas": 1,
"high": 0,
"informational": 0,
"low": 3,
"medium": 0
},
"lines_analyzed_count": 16,
"scan_time_taken": 0,
"score": "2.81",
"score_v2": "56.25",
"threat_score": "100.00"
}
}
},
{
"address": "0x4ACd4BC402bc8e6BA8aBDdcA639d8011ef0b8a4b",
"isVerified": true,
"solidityScanReport": {
"connection_id": "",
"contract_address": "0x4ACd4BC402bc8e6BA8aBDdcA639d8011ef0b8a4b",
"contract_chain": "eth",
"contract_platform": "blockscout",
"contract_url": "https://eth.blockscout.com/address/0x4ACd4BC402bc8e6BA8aBDdcA639d8011ef0b8a4b",
"contractname": "DCAHubPositionDescriptor",
"is_quick_scan": true,
"node_reference_id": null,
"request_type": "threat_scan",
"scanner_reference_url": "https://solidityscan.com/quickscan/0x4ACd4BC402bc8e6BA8aBDdcA639d8011ef0b8a4b/blockscout/optimism?ref=blockscout",
"scan_status": "scan_done",
"scan_summary": {
"issue_severity_distribution": {
"critical": 0,
"gas": 1,
"high": 1,
"informational": 2,
"low": 3,
"medium": 0
},
"lines_analyzed_count": 280,
"scan_time_taken": 1,
"score": "4.77",
"score_v2": "95.36",
"threat_score": "100.00"
}
}
},
{
"address": "0x49c590F6a2dfB0f809E82B9e2BF788C0Dd1c31f9",
"isVerified": true,
"solidityScanReport": {
"connection_id": "",
"contract_address": "0x49c590F6a2dfB0f809E82B9e2BF788C0Dd1c31f9",
"contract_chain": "optimism",
"contract_platform": "blockscout",
"contract_url": "https://optimism.blockscout.com/address/0x49c590F6a2dfB0f809E82B9e2BF788C0Dd1c31f9",
"contractname": "DCAHubCompanion",
"is_quick_scan": true,
"node_reference_id": null,
"request_type": "threat_scan",
"scanner_reference_url": "https://solidityscan.com/quickscan/0x49c590F6a2dfB0f809E82B9e2BF788C0Dd1c31f9/blockscout/optimism?ref=blockscout",
"scan_status": "scan_done",
"scan_summary": {
"issue_severity_distribution": {
"critical": 0,
"gas": 1,
"high": 0,
"informational": 0,
"low": 3,
"medium": 0
},
"lines_analyzed_count": 11,
"scan_time_taken": 0,
"score": "1.82",
"score_v2": "36.36",
"threat_score": "100.00"
}
}
},
{
"address": "0x5ad2fED59E8DF461c6164c31B4267Efb7cBaF9C0",
"isVerified": false,
"solidityScanReport": null
}
]
},
"8453": {
"overallInfo": {
"verifiedNumber": 4,
"totalContractsNumber": 6,
"solidityScanContractsNumber": 4,
"securityScore": 74.88,
"issueSeverityDistribution": {
"critical": 6,
"gas": 25,
"high": 1,
"informational": 7,
"low": 20,
"medium": 3
}
},
"contractsData": [
{
"address": "0xA5AdC5484f9997fBF7D405b9AA62A7d88883C345",
"isVerified": true,
"solidityScanReport": {
"connection_id": "",
"contract_address": "0xA5AdC5484f9997fBF7D405b9AA62A7d88883C345",
"contract_chain": "base",
"contract_platform": "blockscout",
"contract_url": "https://base.blockscout.com/address/0xA5AdC5484f9997fBF7D405b9AA62A7d88883C345",
"contractname": "DCAHub",
"is_quick_scan": true,
"node_reference_id": null,
"request_type": "threat_scan",
"scanner_reference_url": "https://solidityscan.com/quickscan/0xA5AdC5484f9997fBF7D405b9AA62A7d88883C345/blockscout/base?ref=blockscout",
"scan_status": "scan_done",
"scan_summary": {
"issue_severity_distribution": {
"critical": 0,
"gas": 1,
"high": 0,
"informational": 0,
"low": 3,
"medium": 0
},
"lines_analyzed_count": 23,
"scan_time_taken": 0,
"score": "3.48",
"score_v2": "69.57",
"threat_score": "94.44"
}
}
},
{
"address": "0x20bdAE1413659f47416f769a4B27044946bc9923",
"isVerified": true,
"solidityScanReport": {
"connection_id": "",
"contract_address": "0x20bdAE1413659f47416f769a4B27044946bc9923",
"contract_chain": "base",
"contract_platform": "blockscout",
"contract_url": "https://base.blockscout.com/address/0x20bdAE1413659f47416f769a4B27044946bc9923",
"contractname": "DCAPermissionsManager",
"is_quick_scan": true,
"node_reference_id": null,
"request_type": "threat_scan",
"scanner_reference_url": "https://solidityscan.com/quickscan/0x20bdAE1413659f47416f769a4B27044946bc9923/blockscout/base?ref=blockscout",
"scan_status": "scan_done",
"scan_summary": {
"issue_severity_distribution": {
"critical": 2,
"gas": 22,
"high": 0,
"informational": 5,
"low": 11,
"medium": 3
},
"lines_analyzed_count": 314,
"scan_time_taken": 1,
"score": "3.92",
"score_v2": "78.34",
"threat_score": "88.89"
}
}
},
{
"address": "0xDf0dbc66f85979a1d54671c4D9e439F306Be27EE",
"isVerified": true,
"solidityScanReport": {
"connection_id": "",
"contract_address": "0xDf0dbc66f85979a1d54671c4D9e439F306Be27EE",
"contract_chain": "base",
"contract_platform": "blockscout",
"contract_url": "https://base.blockscout.com/address/0xDf0dbc66f85979a1d54671c4D9e439F306Be27EE",
"contractname": "DCAHubCompanion",
"is_quick_scan": true,
"node_reference_id": null,
"request_type": "threat_scan",
"scanner_reference_url": "https://solidityscan.com/quickscan/0xDf0dbc66f85979a1d54671c4D9e439F306Be27EE/blockscout/base?ref=blockscout",
"scan_status": "scan_done",
"scan_summary": {
"issue_severity_distribution": {
"critical": 0,
"gas": 1,
"high": 0,
"informational": 0,
"low": 3,
"medium": 0
},
"lines_analyzed_count": 16,
"scan_time_taken": 0,
"score": "2.81",
"score_v2": "56.25",
"threat_score": "100.00"
}
}
},
{
"address": "0x4ACd4BC402bc8e6BA8aBDdcA639d8011ef0b8a4b",
"isVerified": true,
"solidityScanReport": {
"connection_id": "",
"contract_address": "0x4ACd4BC402bc8e6BA8aBDdcA639d8011ef0b8a4b",
"contract_chain": "eth",
"contract_platform": "blockscout",
"contract_url": "https://eth.blockscout.com/address/0x4ACd4BC402bc8e6BA8aBDdcA639d8011ef0b8a4b",
"contractname": "DCAHubPositionDescriptor",
"is_quick_scan": true,
"node_reference_id": null,
"request_type": "threat_scan",
"scanner_reference_url": "https://solidityscan.com/quickscan/0x4ACd4BC402bc8e6BA8aBDdcA639d8011ef0b8a4b/blockscout/base?ref=blockscout",
"scan_status": "scan_done",
"scan_summary": {
"issue_severity_distribution": {
"critical": 0,
"gas": 1,
"high": 1,
"informational": 2,
"low": 3,
"medium": 0
},
"lines_analyzed_count": 280,
"scan_time_taken": 1,
"score": "4.77",
"score_v2": "95.36",
"threat_score": "100.00"
}
}
},
{
"address": "0x49c590F6a2dfB0f809E82B9e2BF788C0Dd1c31f9",
"isVerified": false,
"solidityScanReport": null
},
{
"address": "0x5ad2fED59E8DF461c6164c31B4267Efb7cBaF9C0",
"isVerified": false,
"solidityScanReport": null
}
]
}
}
},
{
"appName": "cow-swap",
"doc": "https://docs.cow.fi/cow-protocol/reference/contracts/core#deployments",
"chainsData": {
"1": {
"overallInfo": {
"verifiedNumber": 3,
"totalContractsNumber": 3,
"solidityScanContractsNumber": 3,
"securityScore": 87.60000000000001,
"issueSeverityDistribution": {
"critical": 4,
"gas": 18,
"high": 0,
"informational": 13,
"low": 14,
"medium": 3
}
},
"contractsData": [
{
"address": "0x9008D19f58AAbD9eD0D60971565AA8510560ab41",
"isVerified": true,
"solidityScanReport": {
"connection_id": "",
"contract_address": "0x9008D19f58AAbD9eD0D60971565AA8510560ab41",
"contract_chain": "eth",
"contract_platform": "blockscout",
"contract_url": "https://eth.blockscout.com/address/0x9008D19f58AAbD9eD0D60971565AA8510560ab41",
"contractname": "GPv2Settlement",
"is_quick_scan": true,
"node_reference_id": null,
"request_type": "threat_scan",
"scanner_reference_url": "https://solidityscan.com/quickscan/0x9008D19f58AAbD9eD0D60971565AA8510560ab41/blockscout/eth?ref=blockscout",
"scan_status": "scan_done",
"scan_summary": {
"issue_severity_distribution": {
"critical": 0,
"gas": 16,
"high": 0,
"informational": 7,
"low": 5,
"medium": 3
},
"lines_analyzed_count": 493,
"scan_time_taken": 1,
"score": "4.57",
"score_v2": "91.48",
"threat_score": "94.74"
}
}
},
{
"address": "0x2c4c28DDBdAc9C5E7055b4C863b72eA0149D8aFE",
"isVerified": true,
"solidityScanReport": {
"connection_id": "",
"contract_address": "0x2c4c28DDBdAc9C5E7055b4C863b72eA0149D8aFE",
"contract_chain": "eth",
"contract_platform": "blockscout",
"contract_url": "https://eth.blockscout.com/address/0x2c4c28DDBdAc9C5E7055b4C863b72eA0149D8aFE",
"contractname": "EIP173Proxy",
"is_quick_scan": true,
"node_reference_id": null,
"request_type": "threat_scan",
"scanner_reference_url": "https://solidityscan.com/quickscan/0x2c4c28DDBdAc9C5E7055b4C863b72eA0149D8aFE/blockscout/eth?ref=blockscout",
"scan_status": "scan_done",
"scan_summary": {
"issue_severity_distribution": {
"critical": 0,
"gas": 0,
"high": 0,
"informational": 4,
"low": 5,
"medium": 0
},
"lines_analyzed_count": 94,
"scan_time_taken": 0,
"score": "4.26",
"score_v2": "85.11",
"threat_score": "88.89"
}
}
},
{
"address": "0xC92E8bdf79f0507f65a392b0ab4667716BFE0110",
"isVerified": true,
"solidityScanReport": {
"connection_id": "",
"contract_address": "0xC92E8bdf79f0507f65a392b0ab4667716BFE0110",
"contract_chain": "eth",
"contract_platform": "blockscout",
"contract_url": "https://eth.blockscout.com/address/0xC92E8bdf79f0507f65a392b0ab4667716BFE0110",
"contractname": "GPv2VaultRelayer",
"is_quick_scan": true,
"node_reference_id": null,
"request_type": "threat_scan",
"scanner_reference_url": "https://solidityscan.com/quickscan/0xC92E8bdf79f0507f65a392b0ab4667716BFE0110/blockscout/eth?ref=blockscout",
"scan_status": "scan_done",
"scan_summary": {
"issue_severity_distribution": {
"critical": 0,
"gas": 2,
"high": 0,
"informational": 2,
"low": 4,
"medium": 0
},
"lines_analyzed_count": 87,
"scan_time_taken": 0,
"score": "4.31",
"score_v2": "86.21",
"threat_score": "94.74"
}
}
}
]
},
"100": {
"overallInfo": {
"verifiedNumber": 3,
"totalContractsNumber": 3,
"solidityScanContractsNumber": 3,
"securityScore": 87.60000000000001,
"issueSeverityDistribution": {
"critical": 4,
"gas": 18,
"high": 0,
"informational": 13,
"low": 14,
"medium": 3
}
},
"contractsData": [
{
"address": "0x9008D19f58AAbD9eD0D60971565AA8510560ab41",
"isVerified": true,
"solidityScanReport": {
"connection_id": "",
"contract_address": "0x9008D19f58AAbD9eD0D60971565AA8510560ab41",
"contract_chain": "gnosis",
"contract_platform": "blockscout",
"contract_url": "https://gnosis.blockscout.com/address/0x9008D19f58AAbD9eD0D60971565AA8510560ab41",
"contractname": "GPv2Settlement",
"is_quick_scan": true,
"node_reference_id": null,
"request_type": "threat_scan",
"scanner_reference_url": "https://solidityscan.com/quickscan/0x9008D19f58AAbD9eD0D60971565AA8510560ab41/blockscout/gnosis?ref=blockscout",
"scan_status": "scan_done",
"scan_summary": {
"issue_severity_distribution": {
"critical": 0,
"gas": 16,
"high": 0,
"informational": 7,
"low": 5,
"medium": 3
},
"lines_analyzed_count": 493,
"scan_time_taken": 1,
"score": "4.57",
"score_v2": "91.48",
"threat_score": "94.74"
}
}
},
{
"address": "0x2c4c28DDBdAc9C5E7055b4C863b72eA0149D8aFE",
"isVerified": true,
"solidityScanReport": {
"connection_id": "",
"contract_address": "0x2c4c28DDBdAc9C5E7055b4C863b72eA0149D8aFE",
"contract_chain": "eth",
"contract_platform": "blockscout",
"contract_url": "https://eth.blockscout.com/address/0x2c4c28DDBdAc9C5E7055b4C863b72eA0149D8aFE",
"contractname": "EIP173Proxy",
"is_quick_scan": true,
"node_reference_id": null,
"request_type": "threat_scan",
"scanner_reference_url": "https://solidityscan.com/quickscan/0x2c4c28DDBdAc9C5E7055b4C863b72eA0149D8aFE/blockscout/gnosis?ref=blockscout",
"scan_status": "scan_done",
"scan_summary": {
"issue_severity_distribution": {
"critical": 0,
"gas": 0,
"high": 0,
"informational": 4,
"low": 5,
"medium": 0
},
"lines_analyzed_count": 94,
"scan_time_taken": 0,
"score": "4.26",
"score_v2": "85.11",
"threat_score": "88.89"
}
}
},
{
"address": "0xC92E8bdf79f0507f65a392b0ab4667716BFE0110",
"isVerified": true,
"solidityScanReport": {
"connection_id": "",
"contract_address": "0xC92E8bdf79f0507f65a392b0ab4667716BFE0110",
"contract_chain": "eth",
"contract_platform": "blockscout",
"contract_url": "https://eth.blockscout.com/address/0xC92E8bdf79f0507f65a392b0ab4667716BFE0110",
"contractname": "GPv2VaultRelayer",
"is_quick_scan": true,
"node_reference_id": null,
"request_type": "threat_scan",
"scanner_reference_url": "https://solidityscan.com/quickscan/0xC92E8bdf79f0507f65a392b0ab4667716BFE0110/blockscout/gnosis?ref=blockscout",
"scan_status": "scan_done",
"scan_summary": {
"issue_severity_distribution": {
"critical": 0,
"gas": 2,
"high": 0,
"informational": 2,
"low": 4,
"medium": 0
},
"lines_analyzed_count": 87,
"scan_time_taken": 0,
"score": "4.31",
"score_v2": "86.21",
"threat_score": "94.74"
}
}
}
]
}
}
}
]
...@@ -61,8 +61,10 @@ frontend: ...@@ -61,8 +61,10 @@ frontend:
NEXT_PUBLIC_NAME_SERVICE_API_HOST: https://bens-rs-test.k8s-dev.blockscout.com NEXT_PUBLIC_NAME_SERVICE_API_HOST: https://bens-rs-test.k8s-dev.blockscout.com
NEXT_PUBLIC_AUTH_URL: https://blockscout-main.k8s-dev.blockscout.com NEXT_PUBLIC_AUTH_URL: https://blockscout-main.k8s-dev.blockscout.com
NEXT_PUBLIC_MARKETPLACE_ENABLED: true NEXT_PUBLIC_MARKETPLACE_ENABLED: true
NEXT_PUBLIC_MARKETPLACE_CONFIG_URL: https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/marketplace/eth-goerli.json
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM: https://airtable.com/shrqUAcjgGJ4jU88C NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM: https://airtable.com/shrqUAcjgGJ4jU88C
NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM: https://airtable.com/appiy5yijZpMMSKjT/pag3t82DUCyhGRZZO/form 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_LOGOUT_URL: https://blockscoutcom.us.auth0.com/v2/logout
NEXT_PUBLIC_HOMEPAGE_CHARTS: "['daily_txs','coin_price','market_cap']" NEXT_PUBLIC_HOMEPAGE_CHARTS: "['daily_txs','coin_price','market_cap']"
NEXT_PUBLIC_NETWORK_RPC_URL: https://rpc.ankr.com/eth_goerli NEXT_PUBLIC_NETWORK_RPC_URL: https://rpc.ankr.com/eth_goerli
......
...@@ -450,6 +450,7 @@ This feature is **always enabled**, but you can configure its behavior by passin ...@@ -450,6 +450,7 @@ This feature is **always enabled**, but you can configure its behavior by passin
| NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM | `string` | Link to form where users can suggest ideas for the marketplace | - | - | `https://airtable.com/appiy5yijZpMMSKjT/pag3t82DUCyhGRZZO/form` | | NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM | `string` | Link to form where users can suggest ideas for the marketplace | - | - | `https://airtable.com/appiy5yijZpMMSKjT/pag3t82DUCyhGRZZO/form` |
| NEXT_PUBLIC_NETWORK_RPC_URL | `string` | See in [Blockchain parameters](ENVS.md#blockchain-parameters) section | Required | - | `https://core.poa.network` | | NEXT_PUBLIC_NETWORK_RPC_URL | `string` | See in [Blockchain parameters](ENVS.md#blockchain-parameters) section | Required | - | `https://core.poa.network` |
| NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL | `string` | URL of configuration file (`.json` format only) which contains the list of categories to be displayed on the marketplace page in the specified order. If no URL is provided, then the list of categories will be compiled based on the `categories` fields from the marketplace (apps) configuration file | - | - | `https://example.com/marketplace_categories.json` | | NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL | `string` | URL of configuration file (`.json` format only) which contains the list of categories to be displayed on the marketplace page in the specified order. If no URL is provided, then the list of categories will be compiled based on the `categories` fields from the marketplace (apps) configuration file | - | - | `https://example.com/marketplace_categories.json` |
| NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL | `string` | URL of configuration file (`.json` format only) which contains app security reports for displaying security scores on the Marketplace page | - | - | `https://example.com/marketplace_security_reports.json` |
#### Marketplace app configuration properties #### Marketplace app configuration properties
......
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.167 4.167c0-.92.746-1.667 1.667-1.667h3.333c.92 0 1.667.746 1.667 1.667V7.5c0 .92-.747 1.667-1.667 1.667H3.834c-.92 0-1.667-.747-1.667-1.667V4.167Zm1.667.5a.5.5 0 0 1 .5-.5h2.333a.5.5 0 0 1 .5.5V7a.5.5 0 0 1-.5.5H4.334a.5.5 0 0 1-.5-.5V4.667ZM10.5 4.27c0 .519.42.938.938.938h4.792a.938.938 0 0 0 0-1.875h-4.792a.937.937 0 0 0-.938.938Zm0 3.334c0 .518.42.938.938.938h4.792a.938.938 0 0 0 0-1.875h-4.792a.937.937 0 0 0-.938.937ZM2.167 12.5c0-.92.746-1.667 1.667-1.667h3.333c.92 0 1.667.746 1.667 1.667v3.333c0 .92-.747 1.667-1.667 1.667H3.834c-.92 0-1.667-.746-1.667-1.667V12.5Zm1.667.5a.5.5 0 0 1 .5-.5h2.333a.5.5 0 0 1 .5.5v2.333a.5.5 0 0 1-.5.5H4.334a.5.5 0 0 1-.5-.5V13Zm6.666-.396c0 .518.42.938.938.938h4.792a.938.938 0 0 0 0-1.875h-4.792a.937.937 0 0 0-.938.937Zm0 3.334c0 .517.42.937.938.937h4.792a.938.938 0 0 0 0-1.875h-4.792a.937.937 0 0 0-.938.938Z" fill="currentColor"/>
</svg>
<svg viewBox="0 0 21 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M3 4.167c0-.92.746-1.667 1.667-1.667H8c.92 0 1.667.746 1.667 1.667V7.5c0 .92-.747 1.667-1.667 1.667H4.667C3.747 9.167 3 8.42 3 7.5V4.167Zm1.667.5a.5.5 0 0 1 .5-.5H7.5a.5.5 0 0 1 .5.5V7a.5.5 0 0 1-.5.5H5.167a.5.5 0 0 1-.5-.5V4.667ZM3 12.5c0-.92.746-1.667 1.667-1.667H8c.92 0 1.667.746 1.667 1.667v3.333c0 .92-.747 1.667-1.667 1.667H4.667C3.747 17.5 3 16.754 3 15.833V12.5Zm1.667.5a.5.5 0 0 1 .5-.5H7.5a.5.5 0 0 1 .5.5v2.333a.5.5 0 0 1-.5.5H5.167a.5.5 0 0 1-.5-.5V13ZM13 2.5c-.92 0-1.667.746-1.667 1.667V7.5c0 .92.746 1.667 1.667 1.667h3.333C17.253 9.167 18 8.42 18 7.5V4.167c0-.92-.746-1.667-1.667-1.667H13Zm3.333 2.167a.5.5 0 0 0-.5-.5H13.5a.5.5 0 0 0-.5.5V7a.5.5 0 0 0 .5.5h2.333a.5.5 0 0 0 .5-.5V4.667ZM11.333 12.5c0-.92.746-1.667 1.667-1.667h3.333c.92 0 1.667.746 1.667 1.667v3.333c0 .92-.746 1.667-1.667 1.667H13c-.92 0-1.667-.746-1.667-1.667V12.5ZM13 13a.5.5 0 0 1 .5-.5h2.333a.5.5 0 0 1 .5.5v2.333a.5.5 0 0 1-.5.5H13.5a.5.5 0 0 1-.5-.5V13Z" fill="currentColor"/>
</svg>
<svg viewBox="0 0 26 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="26" height="12" rx="2" fill="#F56565"/>
<path d="M3.028 9V1.727h1.061V4.43h.064c.062-.114.15-.245.267-.394.116-.15.277-.28.483-.391.205-.114.478-.17.816-.17.44 0 .834.11 1.18.333.345.223.616.544.812.963.2.419.299.923.299 1.512 0 .59-.098 1.095-.295 1.517a2.3 2.3 0 0 1-.81.97 2.097 2.097 0 0 1-1.175.337c-.332 0-.603-.056-.813-.167a1.54 1.54 0 0 1-.49-.391 2.957 2.957 0 0 1-.274-.398h-.089V9H3.028Zm1.04-2.727c0 .383.056.72.167 1.008.111.29.272.515.483.679.21.16.469.241.774.241.317 0 .582-.084.795-.252.214-.17.375-.401.483-.693.112-.29.167-.619.167-.983 0-.36-.054-.683-.163-.97a1.484 1.484 0 0 0-.483-.678c-.213-.166-.48-.249-.799-.249-.308 0-.568.08-.781.238-.21.159-.37.38-.48.664a2.77 2.77 0 0 0-.163.995Zm7.485 2.837c-.538 0-1-.115-1.389-.344a2.336 2.336 0 0 1-.894-.977c-.209-.421-.313-.915-.313-1.48 0-.56.104-1.052.313-1.478.21-.426.504-.759.88-.998.379-.239.822-.359 1.328-.359.308 0 .606.051.895.153.29.102.548.262.778.48.23.217.41.5.543.848.133.346.2.766.2 1.26v.377H9.556v-.795h3.296c0-.28-.057-.527-.17-.742a1.29 1.29 0 0 0-1.198-.703c-.298 0-.558.073-.78.22-.221.144-.391.334-.512.568a1.641 1.641 0 0 0-.178.756v.622c0 .364.064.674.192.93.13.256.311.451.543.586.232.133.503.199.814.199.2 0 .384-.028.55-.085a1.143 1.143 0 0 0 .707-.692l1.005.18a1.82 1.82 0 0 1-.434.778 2.1 2.1 0 0 1-.777.515 2.91 2.91 0 0 1-1.062.181Zm6.064-5.565v.853h-2.979v-.853h2.98Zm-2.18-1.306h1.062v5.16c0 .205.03.36.092.465.062.101.14.171.238.21.1.035.207.052.323.052.085 0 .16-.005.224-.017l.149-.029.192.877a2.08 2.08 0 0 1-.689.114 1.87 1.87 0 0 1-.781-.15 1.34 1.34 0 0 1-.586-.482c-.15-.218-.224-.491-.224-.82v-5.38Zm4.942 6.882c-.345 0-.658-.064-.937-.192a1.58 1.58 0 0 1-.664-.565c-.161-.246-.242-.548-.242-.905 0-.308.06-.561.178-.76a1.31 1.31 0 0 1 .48-.472c.2-.116.425-.204.674-.263.248-.06.502-.104.76-.135l.795-.092c.204-.027.352-.068.444-.125.092-.057.139-.149.139-.277V5.31c0-.31-.088-.55-.263-.72-.173-.171-.431-.256-.774-.256-.358 0-.64.08-.845.238-.204.156-.345.33-.423.522l-.998-.228a1.92 1.92 0 0 1 .519-.802c.23-.206.493-.355.791-.448.299-.094.612-.142.941-.142.218 0 .45.026.693.079.246.05.476.142.689.277.215.134.392.327.53.578.136.249.205.572.205.97V9h-1.037v-.746h-.043a1.512 1.512 0 0 1-.308.405 1.642 1.642 0 0 1-.53.33 2.053 2.053 0 0 1-.774.132Zm.231-.853c.294 0 .545-.058.753-.174.21-.116.37-.267.48-.454.11-.19.166-.392.166-.607V6.33a.552.552 0 0 1-.22.106 3.43 3.43 0 0 1-.366.082l-.401.06c-.13.017-.24.03-.327.043a2.63 2.63 0 0 0-.564.131.97.97 0 0 0-.405.266.665.665 0 0 0-.15.455c0 .263.098.462.292.597.194.132.441.198.742.198Z" fill="#fff"/>
</svg>
<svg viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="12" height="12" rx="2" fill="#F56565"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.42 2.494c-.373-.313-.896-.49-1.5-.49a2.028 2.028 0 0 0-1.52.567 1.948 1.948 0 0 0-.453.685c-.1.257-.143.531-.127.805V10h.79V8.508a2.46 2.46 0 0 0 1.59.549c.3.01.6-.04.88-.147a2.21 2.21 0 0 0 .751-.48c.215-.208.383-.457.495-.733.112-.274.165-.568.157-.863a1.925 1.925 0 0 0-.309-1.09 1.978 1.978 0 0 0-.717-.664c.157-.147.287-.32.381-.514.12-.245.18-.515.176-.787 0-.523-.22-.97-.594-1.285Zm-2.023.36c.163-.06.338-.085.512-.074h.008c.413 0 .741.105.964.28.22.171.343.417.343.72v.004a.944.944 0 0 1-.288.713.99.99 0 0 1-.732.283l-.115-.004v.781h.112c.467 0 .84.13 1.094.351.253.219.398.534.398.927v.004a1.357 1.357 0 0 1-.418 1.04 1.422 1.422 0 0 1-1.069.4h-.012a1.532 1.532 0 0 1-1.109-.393 1.437 1.437 0 0 1-.475-1.052V4.05a1.165 1.165 0 0 1 .352-.923c.123-.12.272-.214.435-.274Z" fill="#fff"/>
</svg>
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.4 2a1.8 1.8 0 0 0-1.8 1.8v14.8A1.4 1.4 0 0 0 3 20h1.6v-1.326H3V3.516h1.6V2H3.4Z" fill="currentColor"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.992.45C4.244.163 4.585 0 4.94 0h8.038a.63.63 0 0 1 .474.225L18.14 5.61a.83.83 0 0 1 .196.544v12.308c0 .408-.141.799-.392 1.087-.252.289-.593.451-.948.451H4.94c-.356 0-.696-.162-.948-.45a1.661 1.661 0 0 1-.392-1.088V1.538c0-.408.141-.799.392-1.087Zm.948 1.088h6.87v4.497c0 .388.315.702.702.702h4.485v11.725H4.94V1.538Zm8.274.59 2.791 3.205h-2.79V2.128Z" fill="currentColor"/>
<rect x="7.2" y="14.3" width="7.8" height="1.2" rx=".6" fill="currentColor"/>
<rect x="7.2" y="12" width="7.8" height="1.2" rx=".6" fill="currentColor"/>
</svg>
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.4 2a1.8 1.8 0 0 0-1.8 1.8v14.8A1.4 1.4 0 0 0 3 20h1.6v-1.326H3V3.516h1.6V2H3.4Z" fill="currentColor"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.992.45C4.244.163 4.585 0 4.94 0h8.038a.63.63 0 0 1 .474.225L18.14 5.61a.83.83 0 0 1 .196.544v12.308c0 .408-.141.799-.392 1.087-.252.289-.593.451-.948.451H4.94c-.356 0-.696-.162-.948-.45a1.661 1.661 0 0 1-.392-1.088V1.538c0-.408.141-.799.392-1.087Zm8.709 1.088H4.94v16.924h12.057V6.472l-4.296-4.934Z" fill="currentColor"/>
<path d="m7.9 13.357 2.2 2.357L14.5 11" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.512.42c.388 0 .702.315.702.703v4.21h4.21a.702.702 0 1 1 0 1.404h-4.912a.702.702 0 0 1-.702-.702V1.123c0-.388.315-.702.702-.702Z" fill="currentColor"/>
</svg>
<svg viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.76 17.333a.603.603 0 0 1-.294-.075L9 14.798l-4.467 2.46a.606.606 0 0 1-.663-.051.657.657 0 0 1-.213-.285.69.69 0 0 1-.038-.36l.854-5.21-3.616-3.69a.69.69 0 0 1-.16-.677.663.663 0 0 1 .194-.301c.09-.08.2-.131.316-.149l4.994-.76 2.234-4.74a.65.65 0 0 1 .232-.269.61.61 0 0 1 .666 0c.1.065.18.158.233.269l2.233 4.74 4.994.76c.117.018.226.07.316.149a.66.66 0 0 1 .193.3.69.69 0 0 1-.16.678l-3.614 3.69.853 5.21a.692.692 0 0 1-.14.537.634.634 0 0 1-.216.173.605.605 0 0 1-.265.061Z" fill="currentColor"/> <path d="M14.76 18.333a.603.603 0 0 1-.294-.075L10 15.798l-4.467 2.46a.607.607 0 0 1-.663-.052.657.657 0 0 1-.213-.285.69.69 0 0 1-.038-.36l.853-5.21-3.615-3.69a.69.69 0 0 1-.16-.677.663.663 0 0 1 .194-.3c.09-.08.199-.131.315-.149l4.995-.76 2.233-4.74a.65.65 0 0 1 .233-.269.61.61 0 0 1 .666 0c.1.065.18.158.232.269l2.234 4.74 4.994.76c.116.018.226.07.316.149.09.079.157.183.193.3a.69.69 0 0 1-.16.678l-3.615 3.69.854 5.21a.692.692 0 0 1-.14.537.636.636 0 0 1-.216.173.607.607 0 0 1-.266.061h.001Z" fill="currentColor"/>
</svg> </svg>
<svg viewBox="0 0 18 18" fill="currentColor" xmlns="http://www.w3.org/2000/svg"> <svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.76 17.333a.604.604 0 0 1-.294-.075l.293.075Zm.004 0a.625.625 0 0 0 .477-.234.671.671 0 0 0 .14-.538l-.853-5.21 3.615-3.689a.69.69 0 0 0 .16-.677.663.663 0 0 0-.194-.301.617.617 0 0 0-.316-.149l-4.884-.743a.208.208 0 0 1-.157-.117l-2.186-4.64a.65.65 0 0 0-.233-.269.61.61 0 0 0-.666 0 .65.65 0 0 0-.232.269l-2.186 4.64a.208.208 0 0 1-.158.117l-4.884.743a.618.618 0 0 0-.316.149.663.663 0 0 0-.193.3.69.69 0 0 0 .16.678l3.54 3.614a.208.208 0 0 1 .058.18l-.837 5.105a.69.69 0 0 0 .038.36.657.657 0 0 0 .213.286.613.613 0 0 0 .663.05L8.9 14.854a.208.208 0 0 1 .2 0l4.366 2.405m-7.795-2.915c-.028.172.154.3.307.216L8.9 12.95a.208.208 0 0 1 .2 0l2.923 1.61a.208.208 0 0 0 .306-.216l-.566-3.452a.208.208 0 0 1 .057-.18l2.486-2.536a.208.208 0 0 0-.118-.351l-3.408-.519a.208.208 0 0 1-.157-.117L9.189 4.145a.208.208 0 0 0-.377 0L7.378 7.19a.208.208 0 0 1-.158.117l-3.408.519a.208.208 0 0 0-.117.351l2.485 2.537a.208.208 0 0 1 .057.18l-.566 3.45Zm8.093 2.99h-.003.003Z"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M14.76 18.333a.603.603 0 0 1-.294-.075l.293.075Zm.003 0c.09 0 .18-.021.262-.061.083-.04.157-.1.216-.173a.674.674 0 0 0 .14-.538l-.854-5.21 3.616-3.69a.69.69 0 0 0 .16-.677.662.662 0 0 0-.194-.3.617.617 0 0 0-.316-.149l-4.884-.743a.208.208 0 0 1-.158-.117l-2.186-4.64a.651.651 0 0 0-.232-.269.61.61 0 0 0-.666 0 .65.65 0 0 0-.233.269l-2.186 4.64a.208.208 0 0 1-.157.117l-4.885.743a.617.617 0 0 0-.315.149.663.663 0 0 0-.194.3.69.69 0 0 0 .16.678L5.4 12.276a.208.208 0 0 1 .056.18L4.62 17.56a.69.69 0 0 0 .038.36.657.657 0 0 0 .213.285.613.613 0 0 0 .663.052l4.366-2.405a.208.208 0 0 1 .201 0l4.366 2.405m-7.795-2.915c-.028.172.154.3.306.216L9.9 13.95a.208.208 0 0 1 .201 0l2.922 1.61a.208.208 0 0 0 .306-.216l-.565-3.452a.208.208 0 0 1 .057-.18l2.485-2.536a.208.208 0 0 0-.117-.351l-3.409-.52a.208.208 0 0 1-.157-.116l-1.434-3.044a.208.208 0 0 0-.377 0L8.377 8.189a.208.208 0 0 1-.157.117l-3.408.518a.208.208 0 0 0-.118.352l2.486 2.537a.208.208 0 0 1 .057.18l-.566 3.45Zm8.092 2.99h-.003.003Z" fill="currentColor"/>
</svg> </svg>
...@@ -7,6 +7,7 @@ import { STORAGE_KEY, STORAGE_LIMIT } from './consts'; ...@@ -7,6 +7,7 @@ import { STORAGE_KEY, STORAGE_LIMIT } from './consts';
export interface GrowthBookFeatures { export interface GrowthBookFeatures {
test_value: string; test_value: string;
security_score_exp: boolean;
} }
export const growthBook = (() => { export const growthBook = (() => {
......
...@@ -98,8 +98,12 @@ Type extends EventTypes.PAGE_WIDGET ? ( ...@@ -98,8 +98,12 @@ Type extends EventTypes.PAGE_WIDGET ? (
{ {
'Type': 'Tokens dropdown' | 'Tokens show all (icon)' | 'Add to watchlist' | 'Address actions (more button)'; 'Type': 'Tokens dropdown' | 'Tokens show all (icon)' | 'Add to watchlist' | 'Address actions (more button)';
} | { } | {
'Type': 'Favorite app' | 'More button'; 'Type': 'Favorite app' | 'More button' | 'Security score' | 'Total contracts' | 'Verified contracts' | 'Analyzed contracts';
'Info': string; 'Info': string;
'Source': 'Discovery view' | 'Security view' | 'App modal' | 'App page' | 'Security score popup';
} | {
'Type': 'Security score';
'Source': 'Analyzed contracts popup';
} }
) : ) :
Type extends EventTypes.TX_INTERPRETATION_INTERACTION ? { Type extends EventTypes.TX_INTERPRETATION_INTERACTION ? {
......
export const securityReports = [
{
appName: 'token-approval-tracker',
doc: 'http://docs.li.fi/smart-contracts/deployments#mainnet',
chainsData: {
'1': {
overallInfo: {
verifiedNumber: 1,
totalContractsNumber: 1,
solidityScanContractsNumber: 1,
securityScore: 87.5,
issueSeverityDistribution: {
critical: 4,
gas: 1,
high: 0,
informational: 4,
low: 2,
medium: 0,
},
},
contractsData: [
{
address: '0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE',
isVerified: true,
solidityScanReport: {
connection_id: '',
contract_address: '0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE',
contract_chain: 'optimism',
contract_platform: 'blockscout',
contract_url: 'http://optimism.blockscout.com/address/0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE',
contractname: 'LiFiDiamond',
is_quick_scan: true,
node_reference_id: null,
request_type: 'threat_scan',
scanner_reference_url: 'http://solidityscan.com/quickscan/0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE/blockscout/eth?ref=blockscout',
scan_status: 'scan_done',
scan_summary: {
issue_severity_distribution: {
critical: 0,
gas: 1,
high: 0,
informational: 4,
low: 2,
medium: 0,
},
lines_analyzed_count: 72,
scan_time_taken: 1,
score: '4.38',
score_v2: '87.50',
threat_score: '100.00',
},
},
},
],
},
},
},
];
...@@ -4,6 +4,8 @@ ...@@ -4,6 +4,8 @@
| "ABI_slim" | "ABI_slim"
| "ABI" | "ABI"
| "API" | "API"
| "apps_list"
| "apps_xs"
| "apps" | "apps"
| "arrows/down-right" | "arrows/down-right"
| "arrows/east-mini" | "arrows/east-mini"
...@@ -11,6 +13,8 @@ ...@@ -11,6 +13,8 @@
| "arrows/north-east" | "arrows/north-east"
| "arrows/south-east" | "arrows/south-east"
| "arrows/up-down" | "arrows/up-down"
| "beta_xs"
| "beta"
| "blob" | "blob"
| "blobs/image" | "blobs/image"
| "blobs/raw" | "blobs/raw"
...@@ -27,6 +31,8 @@ ...@@ -27,6 +31,8 @@
| "collection" | "collection"
| "contract_verified" | "contract_verified"
| "contract" | "contract"
| "contracts_verified"
| "contracts"
| "copy" | "copy"
| "cross" | "cross"
| "delete" | "delete"
......
...@@ -65,6 +65,7 @@ export const VERIFIED_CONTRACTS_COUNTERS: VerifiedContractsCounters = { ...@@ -65,6 +65,7 @@ export const VERIFIED_CONTRACTS_COUNTERS: VerifiedContractsCounters = {
export const SOLIDITYSCAN_REPORT: SolidityscanReport = { export const SOLIDITYSCAN_REPORT: SolidityscanReport = {
scan_report: { scan_report: {
contractname: 'BullRunners',
scan_status: 'scan_done', scan_status: 'scan_done',
scan_summary: { scan_summary: {
issue_severity_distribution: { issue_severity_distribution: {
......
...@@ -178,6 +178,7 @@ export interface SmartContractVerificationError { ...@@ -178,6 +178,7 @@ export interface SmartContractVerificationError {
export type SolidityscanReport = { export type SolidityscanReport = {
scan_report: { scan_report: {
contractname: string;
scan_status: string; scan_status: string;
scan_summary: { scan_summary: {
issue_severity_distribution: { issue_severity_distribution: {
......
import type { SolidityscanReport } from 'types/api/contract';
export type MarketplaceAppPreview = { export type MarketplaceAppPreview = {
id: string; id: string;
external?: boolean; external?: boolean;
...@@ -24,7 +26,45 @@ export type MarketplaceAppOverview = MarketplaceAppPreview & MarketplaceAppSocia ...@@ -24,7 +26,45 @@ export type MarketplaceAppOverview = MarketplaceAppPreview & MarketplaceAppSocia
site?: string; site?: string;
} }
export type MarketplaceAppWithSecurityReport = MarketplaceAppOverview & {
securityReport?: MarketplaceAppSecurityReport;
}
export enum MarketplaceCategory { export enum MarketplaceCategory {
ALL = 'All', ALL = 'All',
FAVORITES = 'Favorites', FAVORITES = 'Favorites',
} }
export enum ContractListTypes {
ANALYZED = 'Analyzed',
ALL = 'All',
VERIFIED = 'Verified',
}
export enum MarketplaceDisplayType {
DEFAULT = 'default',
SCORES = 'scores',
}
export type MarketplaceAppSecurityReport = {
overallInfo: {
verifiedNumber: number;
totalContractsNumber: number;
solidityScanContractsNumber: number;
securityScore: number;
totalIssues?: number;
issueSeverityDistribution: SolidityscanReport['scan_report']['scan_summary']['issue_severity_distribution'];
};
contractsData: Array<{
address: string;
isVerified: boolean;
solidityScanReport?: SolidityscanReport['scan_report'] | null;
}>;
}
export type MarketplaceAppSecurityReportRaw = {
appName: string;
chainsData: {
[chainId: string]: MarketplaceAppSecurityReport;
};
}
import { import { Box, Text, chakra, Icon, Popover, PopoverTrigger, PopoverContent, PopoverBody, useDisclosure } from '@chakra-ui/react';
Box,
Flex,
Text,
Grid,
Button,
chakra,
Popover,
PopoverTrigger,
PopoverBody,
PopoverContent,
useDisclosure,
Skeleton,
Center,
useColorModeValue,
Icon,
} from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { SolidityscanReport as TSolidityscanReport } from 'types/api/contract';
// This icon doesn't work properly when it is in the sprite // This icon doesn't work properly when it is in the sprite
// Probably because of the gradient // Probably because of the gradient
// eslint-disable-next-line no-restricted-imports // eslint-disable-next-line no-restricted-imports
import solidityScanIcon from 'icons/brands/solidity_scan.svg'; import solidityScanIcon from 'icons/brands/solidity_scan.svg';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import { SOLIDITYSCAN_REPORT } from 'stubs/contract'; import { SOLIDITYSCAN_REPORT } from 'stubs/contract';
import IconSvg from 'ui/shared/IconSvg';
import LinkExternal from 'ui/shared/LinkExternal'; import LinkExternal from 'ui/shared/LinkExternal';
import SolidityscanReportButton from 'ui/shared/solidityscanReport/SolidityscanReportButton';
type DistributionItem = { import SolidityscanReportDetails from 'ui/shared/solidityscanReport/SolidityscanReportDetails';
id: keyof TSolidityscanReport['scan_report']['scan_summary']['issue_severity_distribution']; import SolidityscanReportScore from 'ui/shared/solidityscanReport/SolidityscanReportScore';
name: string;
color: string;
}
const DISTRIBUTION_ITEMS: Array<DistributionItem> = [
{ id: 'critical', name: 'Critical', color: '#891F11' },
{ id: 'high', name: 'High', color: '#EC672C' },
{ id: 'medium', name: 'Medium', color: '#FBE74D' },
{ id: 'low', name: 'Low', color: '#68C88E' },
{ id: 'informational', name: 'Informational', color: '#A3AEBE' },
{ id: 'gas', name: 'Gas', color: '#A47585' },
];
interface Props { interface Props {
className?: string; className?: string;
hash: string; hash: string;
} }
type ItemProps = {
item: DistributionItem;
vulnerabilities: TSolidityscanReport['scan_report']['scan_summary']['issue_severity_distribution'];
vulnerabilitiesCount: number;
}
const SolidityScanReportItem = ({ item, vulnerabilities, vulnerabilitiesCount }: ItemProps) => {
const bgBar = useColorModeValue('blackAlpha.50', 'whiteAlpha.50');
const yetAnotherGrayColor = useColorModeValue('gray.400', 'gray.500');
return (
<>
<Box w={ 3 } h={ 3 } bg={ item.color } borderRadius="6px" mr={ 2 }></Box>
<Flex justifyContent="space-between" mr={ 3 }>
<Text>{ item.name }</Text>
<Text color={ vulnerabilities[item.id] > 0 ? 'text' : yetAnotherGrayColor }>{ vulnerabilities[item.id] }</Text>
</Flex>
<Box bg={ bgBar } h="10px" borderRadius="8px">
<Box bg={ item.color } w={ vulnerabilities[item.id] / vulnerabilitiesCount } h="10px" borderRadius="8px"/>
</Box>
</>
);
};
const SolidityscanReport = ({ className, hash }: Props) => { const SolidityscanReport = ({ className, hash }: Props) => {
const { isOpen, onToggle, onClose } = useDisclosure(); const { isOpen, onToggle, onClose } = useDisclosure();
...@@ -85,31 +30,10 @@ const SolidityscanReport = ({ className, hash }: Props) => { ...@@ -85,31 +30,10 @@ const SolidityscanReport = ({ className, hash }: Props) => {
const score = Number(data?.scan_report.scan_summary.score_v2); const score = Number(data?.scan_report.scan_summary.score_v2);
const chartGrayColor = useColorModeValue('gray.100', 'gray.700');
const yetAnotherGrayColor = useColorModeValue('gray.400', 'gray.500');
const popoverBgColor = useColorModeValue('white', 'gray.900');
const greatScoreColor = useColorModeValue('green.600', 'green.400');
const averageScoreColor = useColorModeValue('purple.600', 'purple.400');
const lowScoreColor = useColorModeValue('red.600', 'red.400');
if (isError || !score) { if (isError || !score) {
return null; return null;
} }
let scoreColor;
let scoreLevel;
if (score >= 80) {
scoreColor = greatScoreColor;
scoreLevel = 'GREAT';
} else if (score >= 30) {
scoreColor = averageScoreColor;
scoreLevel = 'AVERAGE';
} else {
scoreColor = lowScoreColor;
scoreLevel = 'LOW';
}
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 vulnerabilitiesCounts = vulnerabilities ? Object.values(vulnerabilities) : [];
const vulnerabilitiesCount = vulnerabilitiesCounts.reduce((acc, val) => acc + val, 0); const vulnerabilitiesCount = vulnerabilitiesCounts.reduce((acc, val) => acc + val, 0);
...@@ -117,24 +41,12 @@ const SolidityscanReport = ({ className, hash }: Props) => { ...@@ -117,24 +41,12 @@ const SolidityscanReport = ({ className, hash }: Props) => {
return ( return (
<Popover isOpen={ isOpen } onClose={ onClose } placement="bottom-start" isLazy> <Popover isOpen={ isOpen } onClose={ onClose } placement="bottom-start" isLazy>
<PopoverTrigger> <PopoverTrigger>
<Skeleton isLoaded={ !isPlaceholderData } borderRadius="base"> <SolidityscanReportButton
<Button className={ className }
className={ className } score={ score }
color={ scoreColor } isLoading={ isPlaceholderData }
size="sm" onClick={ onToggle }
variant="outline" />
colorScheme="gray"
onClick={ onToggle }
aria-label="SolidityScan score"
fontWeight={ 500 }
px="6px"
h="32px"
flexShrink={ 0 }
>
<IconSvg name={ score < 80 ? 'score/score-not-ok' : 'score/score-ok' } boxSize={ 5 } mr={ 1 }/>
{ score }
</Button>
</Skeleton>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent w={{ base: '100vw', lg: '328px' }}> <PopoverContent w={{ base: '100vw', lg: '328px' }}>
<PopoverBody px="26px" py="20px" fontSize="sm"> <PopoverBody px="26px" py="20px" fontSize="sm">
...@@ -143,35 +55,11 @@ const SolidityscanReport = ({ className, hash }: Props) => { ...@@ -143,35 +55,11 @@ const SolidityscanReport = ({ className, hash }: Props) => {
<Icon as={ solidityScanIcon } mr={ 1 } ml="6px" w="23px" h="20px" display="inline-block" verticalAlign="middle"/> <Icon as={ solidityScanIcon } mr={ 1 } ml="6px" w="23px" h="20px" display="inline-block" verticalAlign="middle"/>
<Text fontWeight={ 600 } display="inline-block">SolidityScan</Text> <Text fontWeight={ 600 } display="inline-block">SolidityScan</Text>
</Box> </Box>
<Flex alignItems="center" mb={ 5 }> <SolidityscanReportScore score={ score } mb={ 5 }/>
<Box
w={ 12 }
h={ 12 }
bgGradient={ `conic-gradient(${ scoreColor } 0, ${ scoreColor } ${ score }%, ${ chartGrayColor } 0, ${ chartGrayColor } 100%)` }
borderRadius="24px"
position="relative"
mr={ 3 }
>
<Center position="absolute" w="38px" h="38px" top="5px" right="5px" bg={ popoverBgColor } borderRadius="20px">
<IconSvg name={ score < 80 ? 'score/score-not-ok' : 'score/score-ok' } boxSize={ 5 } color={ scoreColor }/>
</Center>
</Box>
<Box>
<Flex>
<Text color={ scoreColor } fontSize="lg" fontWeight={ 500 }>{ score }</Text>
<Text color={ yetAnotherGrayColor } fontSize="lg" fontWeight={ 500 } whiteSpace="pre"> / 100</Text>
</Flex>
<Text color={ scoreColor } fontWeight={ 500 }>Security score is { scoreLevel }</Text>
</Box>
</Flex>
{ vulnerabilities && vulnerabilitiesCount > 0 && ( { vulnerabilities && vulnerabilitiesCount > 0 && (
<Box mb={ 5 }> <Box mb={ 5 }>
<Text py="7px" variant="secondary" fontSize="xs" fontWeight={ 500 }>Vulnerabilities distribution</Text> <Text py="7px" variant="secondary" fontSize="xs" fontWeight={ 500 }>Vulnerabilities distribution</Text>
<Grid templateColumns="20px 1fr 100px" alignItems="center" rowGap={ 2 }> <SolidityscanReportDetails vulnerabilities={ vulnerabilities } vulnerabilitiesCount={ vulnerabilitiesCount }/>
{ DISTRIBUTION_ITEMS.map(item => (
<SolidityScanReportItem item={ item } key={ item.id } vulnerabilities={ vulnerabilities } vulnerabilitiesCount={ vulnerabilitiesCount }/>
)) }
</Grid>
</Box> </Box>
) } ) }
<LinkExternal href={ data?.scan_report.scanner_reference_url }>View full report</LinkExternal> <LinkExternal href={ data?.scan_report.scanner_reference_url }>View full report</LinkExternal>
......
import { Box, Text, Link, Popover, PopoverTrigger, PopoverBody, PopoverContent, useDisclosure } from '@chakra-ui/react';
import React from 'react';
import type { MarketplaceAppSecurityReport } from 'types/client/marketplace';
import config from 'configs/app';
import { apos } from 'lib/html-entities';
import * as mixpanel from 'lib/mixpanel/index';
import IconSvg from 'ui/shared/IconSvg';
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?: MarketplaceAppSecurityReport;
height?: string | undefined;
showContractList: () => void;
isLoading?: boolean;
onlyIcon?: boolean;
source: 'Security view' | 'App modal' | 'App page';
}
const AppSecurityReport = ({ id, securityReport, height, showContractList, isLoading, onlyIcon, source }: Props) => {
const { isOpen, onToggle, onClose } = useDisclosure();
const handleButtonClick = React.useCallback(() => {
mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'Security score', Info: id, Source: source });
onToggle();
}, [ id, source, onToggle ]);
const handleLinkClick = React.useCallback(() => {
mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'Analyzed contracts', Info: id, Source: 'Security score popup' });
showContractList();
}, [ id, showContractList ]);
if (!securityReport && !isLoading) {
return null;
}
const {
securityScore = 0,
solidityScanContractsNumber = 0,
issueSeverityDistribution = {} as MarketplaceAppSecurityReport['overallInfo']['issueSeverityDistribution'],
totalIssues = 0,
} = securityReport?.overallInfo || {};
return (
<Popover isOpen={ isOpen } onClose={ onClose } placement="bottom-start" isLazy>
<PopoverTrigger>
<SolidityscanReportButton
score={ securityScore }
isLoading={ isLoading }
onClick={ handleButtonClick }
height={ height }
onlyIcon={ onlyIcon }
/>
</PopoverTrigger>
<PopoverContent w={{ base: '100vw', lg: '328px' }}>
<PopoverBody px="26px" py="20px" fontSize="sm">
<Box mb={ 5 }>
{ solidityScanContractsNumber } smart contract{ solidityScanContractsNumber === 1 ? ' was' : 's were' } evaluated to determine
this protocol{ apos }s overall security score on the { config.chain.name } network.
</Box>
<SolidityscanReportScore score={ securityScore } mb={ 5 }/>
{ 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>
) }
<Link onClick={ handleLinkClick } display="inline-flex" alignItems="center">
Analyzed contracts
<IconSvg name="arrows/north-east" boxSize={ 5 } color="gray.400"/>
</Link>
</PopoverBody>
</PopoverContent>
</Popover>
);
};
export default AppSecurityReport;
import { Link, Tooltip, Skeleton } from '@chakra-ui/react';
import React from 'react';
import type { MouseEvent } from 'react';
import config from 'configs/app';
import IconSvg from 'ui/shared/IconSvg';
export enum ContractListButtonVariants {
ALL_CONTRACTS = 'all contracts',
VERIFIED_CONTRACTS = 'verified contracts',
}
const values = {
[ContractListButtonVariants.ALL_CONTRACTS]: {
icon: 'contracts' as const,
iconColor: 'gray.500',
tooltip: `Total number of contracts deployed by the protocol on ${ config.chain.name }`,
},
[ContractListButtonVariants.VERIFIED_CONTRACTS]: {
icon: 'contracts_verified' as const,
iconColor: 'green.500',
tooltip: `Number of verified contracts on ${ config.chain.name }`,
},
};
interface Props {
children: string | number;
onClick: (event: MouseEvent) => void;
variant: ContractListButtonVariants;
isLoading?: boolean;
}
const ContractListButton = ({ children, onClick, variant, isLoading }: Props) => {
const { icon, iconColor, tooltip } = values[variant];
return (
<Tooltip
label={ tooltip }
textAlign="center"
padding={ 2 }
isDisabled={ !tooltip }
openDelay={ 500 }
width="250px"
>
<Skeleton
isLoaded={ !isLoading }
display="inline-flex"
alignItems="center"
width={ isLoading ? '40px' : 'auto' }
height="30px"
borderRadius="base"
>
<Link
fontSize="sm"
onClick={ onClick }
fontWeight="500"
display="inline-flex"
>
{ icon && <IconSvg name={ icon } boxSize={ 5 } color={ iconColor } mr={ 1 }/> }
{ children }
</Link>
</Skeleton>
</Tooltip>
);
};
export default ContractListButton;
import {
Box, Modal, Text, ModalBody,
ModalCloseButton, ModalContent, ModalHeader, ModalOverlay,
} from '@chakra-ui/react';
import React from 'react';
import type { MarketplaceAppSecurityReport } from 'types/client/marketplace';
import { ContractListTypes } from 'types/client/marketplace';
import useIsMobile from 'lib/hooks/useIsMobile';
import { apos } from 'lib/html-entities';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import IconSvg from 'ui/shared/IconSvg';
import ContractSecurityReport from './ContractSecurityReport';
type Props = {
onClose: () => void;
onBack?: () => void;
type: ContractListTypes;
contracts?: MarketplaceAppSecurityReport['contractsData'];
}
const titles = {
[ContractListTypes.ALL]: `All app${ apos }s smart contracts`,
[ContractListTypes.ANALYZED]: 'Analyzed contracts',
[ContractListTypes.VERIFIED]: 'Verified contracts',
};
const ContractListModal = ({ onClose, onBack, type, contracts }: Props) => {
const isMobile = useIsMobile();
const displayedContracts = React.useMemo(() => {
if (!contracts) {
return [];
}
switch (type) {
default:
case ContractListTypes.ALL:
return contracts;
case ContractListTypes.ANALYZED:
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')),
);
case ContractListTypes.VERIFIED:
return contracts.filter((contract) => contract.isVerified);
}
}, [ contracts, type ]);
if (!contracts) {
return null;
}
return (
<Modal
isOpen={ Boolean(type) }
onClose={ onClose }
size={ isMobile ? 'full' : 'md' }
isCentered
>
<ModalOverlay/>
<ModalContent>
<ModalHeader display="flex" alignItems="center" mb={ 4 }>
{ onBack && (
<IconSvg
name="arrows/east"
w={ 6 }
h={ 10 }
transform="rotate(180deg)"
verticalAlign="middle"
color="gray.400"
mr={ 3 }
cursor="pointer"
onClick={ onBack }
/>
) }
<Text fontWeight="500" textStyle="h3">
{ titles[type] }
</Text>
</ModalHeader>
<ModalCloseButton/>
<ModalBody
maxH={ isMobile ? 'auto' : '352px' }
overflow="scroll"
mb={ 0 }
display="grid"
gridTemplateColumns="max-content 1fr"
rowGap={ 2 }
columnGap={ type === ContractListTypes.ANALYZED ? 4 : 0 }
>
{ displayedContracts.map((contract) => (
<React.Fragment key={ contract.address }>
{ type === ContractListTypes.ANALYZED && (
<Box gridColumn={ 1 }>
<ContractSecurityReport securityReport={ contract.solidityScanReport }/>
</Box>
) }
<AddressEntity
address={{
hash: contract.address,
name: contract.solidityScanReport?.contractname,
is_contract: true,
is_verified: contract.isVerified,
}}
noCopy
gridColumn={ 2 }
height="32px"
/>
</React.Fragment>
)) }
</ModalBody>
</ModalContent>
</Modal>
);
};
export default ContractListModal;
import { Box, Text, Popover, PopoverTrigger, PopoverBody, PopoverContent, useDisclosure } from '@chakra-ui/react';
import React from 'react';
import type { SolidityscanReport } from 'types/api/contract';
import config from 'configs/app';
import * as mixpanel from 'lib/mixpanel/index';
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?: SolidityscanReport['scan_report'] | null;
}
const ContractSecurityReport = ({ securityReport }: Props) => {
const { isOpen, onToggle, onClose } = useDisclosure();
const handleClick = React.useCallback(() => {
mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'Security score', Source: 'Analyzed contracts popup' });
onToggle();
}, [ onToggle ]);
if (!securityReport) {
return null;
}
const url = securityReport?.scanner_reference_url;
const {
score_v2: securityScore,
issue_severity_distribution: issueSeverityDistribution,
} = securityReport.scan_summary;
const totalIssues = Object.values(issueSeverityDistribution as Record<string, number>).reduce((acc, val) => acc + val, 0);
return (
<Popover isOpen={ isOpen } onClose={ onClose } placement="bottom-start" isLazy>
<PopoverTrigger>
<SolidityscanReportButton
score={ parseFloat(securityScore) }
onClick={ handleClick }
/>
</PopoverTrigger>
<PopoverContent w={{ base: '100vw', lg: '328px' }}>
<PopoverBody px="26px" py="20px" fontSize="sm">
<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={ parseFloat(securityScore) } mb={ 5 }/>
{ 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>
</PopoverBody>
</PopoverContent>
</Popover>
);
};
export default ContractSecurityReport;
import React from 'react';
import { MarketplaceCategory } from 'types/client/marketplace';
import config from 'configs/app';
import { apos } from 'lib/html-entities';
import EmptySearchResultDefault from 'ui/shared/EmptySearchResult';
import IconSvg from 'ui/shared/IconSvg';
import LinkExternal from 'ui/shared/LinkExternal';
const feature = config.features.marketplace;
type Props = {
favoriteApps: Array<string>;
selectedCategoryId?: string;
}
const EmptySearchResult = ({ favoriteApps, selectedCategoryId }: Props) => (
<EmptySearchResultDefault
text={
(selectedCategoryId === MarketplaceCategory.FAVORITES && !favoriteApps.length) ? (
<>
You don{ apos }t have any favorite apps.
Click on the <IconSvg name="star_outline" w={ 4 } h={ 4 } mb={ -0.5 }/> icon on the app{ apos }s card to add it to Favorites.
</>
) : (
<>
No matching apps found.
{ 'suggestIdeasFormUrl' in feature && (
<>
{ ' ' }Have a groundbreaking idea or app suggestion?{ ' ' }
<LinkExternal href={ feature.suggestIdeasFormUrl }>Share it with us</LinkExternal>
</>
) }
</>
)
}
/>
);
export default React.memo(EmptySearchResult);
import { Box, IconButton, Image, Link, LinkBox, Skeleton, useColorModeValue, Tooltip } from '@chakra-ui/react'; import { Box, IconButton, Image, Link, LinkBox, Skeleton, useColorModeValue } from '@chakra-ui/react';
import type { MouseEvent } from 'react'; import type { MouseEvent } from 'react';
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import type { MarketplaceAppPreview } from 'types/client/marketplace'; import type { MarketplaceAppPreview } from 'types/client/marketplace';
import * as mixpanel from 'lib/mixpanel/index'; import * as mixpanel from 'lib/mixpanel/index';
import type { IconName } from 'ui/shared/IconSvg';
import IconSvg from 'ui/shared/IconSvg'; import IconSvg from 'ui/shared/IconSvg';
import MarketplaceAppCardLink from './MarketplaceAppCardLink'; import MarketplaceAppCardLink from './MarketplaceAppCardLink';
import MarketplaceAppIntegrationIcon from './MarketplaceAppIntegrationIcon';
interface Props extends MarketplaceAppPreview { interface Props extends MarketplaceAppPreview {
onInfoClick: (id: string) => void; onInfoClick: (id: string) => void;
isFavorite: boolean; isFavorite: boolean;
onFavoriteClick: (id: string, isFavorite: boolean) => void; onFavoriteClick: (id: string, isFavorite: boolean, source: 'Discovery view') => void;
isLoading: boolean; isLoading: boolean;
showDisclaimer: (id: string) => void; onAppClick: (event: MouseEvent, id: string) => void;
} }
const MarketplaceAppCard = ({ const MarketplaceAppCard = ({
...@@ -31,48 +31,23 @@ const MarketplaceAppCard = ({ ...@@ -31,48 +31,23 @@ const MarketplaceAppCard = ({
isFavorite, isFavorite,
onFavoriteClick, onFavoriteClick,
isLoading, isLoading,
showDisclaimer,
internalWallet, internalWallet,
onAppClick,
}: Props) => { }: Props) => {
const categoriesLabel = categories.join(', '); const categoriesLabel = categories.join(', ');
const handleClick = useCallback((event: MouseEvent) => {
const isShown = window.localStorage.getItem('marketplace-disclaimer-shown');
if (!isShown) {
event.preventDefault();
showDisclaimer(id);
}
}, [ showDisclaimer, id ]);
const handleInfoClick = useCallback((event: MouseEvent) => { const handleInfoClick = useCallback((event: MouseEvent) => {
event.preventDefault(); event.preventDefault();
mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'More button', Info: id }); mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'More button', Info: id, Source: 'Discovery view' });
onInfoClick(id); onInfoClick(id);
}, [ onInfoClick, id ]); }, [ onInfoClick, id ]);
const handleFavoriteClick = useCallback(() => { const handleFavoriteClick = useCallback(() => {
onFavoriteClick(id, isFavorite); onFavoriteClick(id, isFavorite, 'Discovery view');
}, [ onFavoriteClick, id, isFavorite ]); }, [ onFavoriteClick, id, isFavorite ]);
const logoUrl = useColorModeValue(logo, logoDarkMode || logo); const logoUrl = useColorModeValue(logo, logoDarkMode || logo);
const [ integrationIcon, integrationIconColor, integrationText ] = React.useMemo(() => {
let icon: IconName = 'integration/partial';
let color = 'gray.400';
let text = 'This app opens in Blockscout without Blockscout wallet functionality. Use your external web3 wallet to connect directly to this application';
if (external) {
icon = 'arrows/north-east';
text = 'This app opens in a separate tab';
} else if (internalWallet) {
icon = 'integration/full';
color = 'green.500';
text = 'This app opens in Blockscout and your Blockscout wallet connects automatically';
}
return [ icon, color, text ];
}, [ external, internalWallet ]);
return ( return (
<LinkBox <LinkBox
_hover={{ _hover={{
...@@ -130,25 +105,9 @@ const MarketplaceAppCard = ({ ...@@ -130,25 +105,9 @@ const MarketplaceAppCard = ({
url={ url } url={ url }
external={ external } external={ external }
title={ title } title={ title }
onClick={ handleClick } onClick={ onAppClick }
/> />
<Tooltip <MarketplaceAppIntegrationIcon external={ external } internalWallet={ internalWallet }/>
label={ integrationText }
textAlign="center"
padding={ 2 }
openDelay={ 300 }
maxW={ 400 }
>
<IconSvg
name={ integrationIcon }
boxSize={ 5 }
color={ integrationIconColor }
position="relative"
cursor="pointer"
verticalAlign="middle"
marginBottom={ 1 }
/>
</Tooltip>
</Skeleton> </Skeleton>
<Skeleton <Skeleton
...@@ -191,8 +150,7 @@ const MarketplaceAppCard = ({ ...@@ -191,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' }}
...@@ -204,8 +162,8 @@ const MarketplaceAppCard = ({ ...@@ -204,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"/>
} }
/> />
) } ) }
......
...@@ -8,17 +8,21 @@ type Props = { ...@@ -8,17 +8,21 @@ type Props = {
url: string; url: string;
external?: boolean; external?: boolean;
title: string; title: string;
onClick?: (event: MouseEvent) => void; onClick?: (event: MouseEvent, id: string) => void;
} }
const MarketplaceAppCardLink = ({ url, external, id, title, onClick }: Props) => { const MarketplaceAppCardLink = ({ url, external, id, title, onClick }: Props) => {
const handleClick = React.useCallback((event: MouseEvent) => {
onClick?.(event, id);
}, [ onClick, id ]);
return external ? ( return external ? (
<LinkOverlay href={ url } isExternal={ true } marginRight={ 2 }> <LinkOverlay href={ url } isExternal={ true } marginRight={ 2 }>
{ title } { title }
</LinkOverlay> </LinkOverlay>
) : ( ) : (
<NextLink href={{ pathname: '/apps/[id]', query: { id } }} passHref legacyBehavior> <NextLink href={{ pathname: '/apps/[id]', query: { id } }} passHref legacyBehavior>
<LinkOverlay onClick={ onClick } marginRight={ 2 }> <LinkOverlay onClick={ handleClick } marginRight={ 2 }>
{ title } { title }
</LinkOverlay> </LinkOverlay>
</NextLink> </NextLink>
......
...@@ -28,7 +28,7 @@ test.describe('mobile', () => { ...@@ -28,7 +28,7 @@ test.describe('mobile', () => {
</TestApp>, </TestApp>,
); );
await page.getByText('Info').click(); await page.getByLabel('Show project info').click();
await expect(page).toHaveScreenshot(); await expect(page).toHaveScreenshot();
}); });
......
...@@ -22,7 +22,7 @@ const MarketplaceAppInfo = ({ data }: Props) => { ...@@ -22,7 +22,7 @@ const MarketplaceAppInfo = ({ data }: Props) => {
if (isMobile) { if (isMobile) {
return ( return (
<> <>
<TriggerButton onClick={ onToggle }/> <TriggerButton onClick={ onToggle } onlyIcon/>
<Modal isOpen={ isOpen } onClose={ onClose } size="full"> <Modal isOpen={ isOpen } onClose={ onClose } size="full">
<ModalContent> <ModalContent>
<ModalCloseButton/> <ModalCloseButton/>
......
...@@ -5,9 +5,10 @@ import IconSvg from 'ui/shared/IconSvg'; ...@@ -5,9 +5,10 @@ import IconSvg from 'ui/shared/IconSvg';
interface Props { interface Props {
onClick: () => void; onClick: () => void;
onlyIcon?: boolean;
} }
const TriggerButton = ({ onClick }: Props, ref: React.ForwardedRef<HTMLButtonElement>) => { const TriggerButton = ({ onClick, onlyIcon }: Props, ref: React.ForwardedRef<HTMLButtonElement>) => {
return ( return (
<Button <Button
ref={ ref } ref={ ref }
...@@ -17,11 +18,11 @@ const TriggerButton = ({ onClick }: Props, ref: React.ForwardedRef<HTMLButtonEle ...@@ -17,11 +18,11 @@ const TriggerButton = ({ onClick }: Props, ref: React.ForwardedRef<HTMLButtonEle
onClick={ onClick } onClick={ onClick }
aria-label="Show project info" aria-label="Show project info"
fontWeight={ 500 } fontWeight={ 500 }
px={ 2 } px={ onlyIcon ? 1 : 2 }
h="32px" h="32px"
> >
<IconSvg name="info" boxSize={ 6 } mr={ 1 }/> <IconSvg name="info" boxSize={ 6 } mr={ onlyIcon ? 0 : 1 }/>
<span>Info</span> { !onlyIcon && <span>Info</span> }
</Button> </Button>
); );
}; };
......
import { Tooltip } from '@chakra-ui/react';
import React from 'react';
import type { IconName } from 'ui/shared/IconSvg';
import IconSvg from 'ui/shared/IconSvg';
type Props = {
internalWallet: boolean | undefined;
external: boolean | undefined;
}
const MarketplaceAppIntegrationIcon = ({ external, internalWallet }: Props) => {
const [ icon, iconColor, text ] = React.useMemo(() => {
let icon: IconName = 'integration/partial';
let color = 'gray.400';
let text = 'This app opens in Blockscout without Blockscout wallet functionality. Use your external web3 wallet to connect directly to this application';
if (external) {
icon = 'arrows/north-east';
text = 'This app opens in a separate tab';
} else if (internalWallet) {
icon = 'integration/full';
color = 'green.500';
text = 'This app opens in Blockscout and your Blockscout wallet connects automatically';
}
return [ icon, color, text ];
}, [ external, internalWallet ]);
return (
<Tooltip
label={ text }
textAlign="center"
padding={ 2 }
openDelay={ 300 }
maxW={ 400 }
>
<IconSvg
name={ icon }
boxSize={ 5 }
color={ iconColor }
position="relative"
cursor="pointer"
verticalAlign="middle"
marginBottom={ 1 }
/>
</Tooltip>
);
};
export default MarketplaceAppIntegrationIcon;
import { test, expect, devices } from '@playwright/experimental-ct-react'; import { test, expect, devices } from '@playwright/experimental-ct-react';
import React from 'react'; import React from 'react';
import type { MarketplaceAppWithSecurityReport } from 'types/client/marketplace';
import { apps as appsMock } from 'mocks/apps/apps'; import { apps as appsMock } from 'mocks/apps/apps';
import TestApp from 'playwright/TestApp'; import TestApp from 'playwright/TestApp';
...@@ -9,7 +11,8 @@ import MarketplaceAppModal from './MarketplaceAppModal'; ...@@ -9,7 +11,8 @@ import MarketplaceAppModal from './MarketplaceAppModal';
const props = { const props = {
onClose: () => {}, onClose: () => {},
onFavoriteClick: () => {}, onFavoriteClick: () => {},
data: appsMock[0], showContractList: () => {},
data: appsMock[0] as MarketplaceAppWithSecurityReport,
isFavorite: false, isFavorite: false,
}; };
......
import { import {
Box, Flex, Heading, IconButton, Image, Link, List, Modal, ModalBody, Box, Flex, Heading, IconButton, Image, Link, List, Modal, ModalBody,
ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay, Tag, Text, useColorModeValue, ModalCloseButton, ModalContent, ModalFooter, ModalOverlay, Tag, Text, useColorModeValue,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import type { MarketplaceAppOverview } from 'types/client/marketplace'; import type { MarketplaceAppWithSecurityReport } from 'types/client/marketplace';
import { ContractListTypes } from 'types/client/marketplace';
import useFeatureValue from 'lib/growthbook/useFeatureValue';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import { nbsp } from 'lib/html-entities'; import { nbsp } from 'lib/html-entities';
import * as mixpanel from 'lib/mixpanel/index';
import type { IconName } from 'ui/shared/IconSvg'; import type { IconName } from 'ui/shared/IconSvg';
import IconSvg from 'ui/shared/IconSvg'; import IconSvg from 'ui/shared/IconSvg';
import AppSecurityReport from './AppSecurityReport';
import ContractListButton, { ContractListButtonVariants } from './ContractListButton';
import MarketplaceAppModalLink from './MarketplaceAppModalLink'; import MarketplaceAppModalLink from './MarketplaceAppModalLink';
type Props = { type Props = {
onClose: () => void; onClose: () => void;
isFavorite: boolean; isFavorite: boolean;
onFavoriteClick: (id: string, isFavorite: boolean) => void; onFavoriteClick: (id: string, isFavorite: boolean, source: 'App modal') => void;
data: MarketplaceAppOverview; data: MarketplaceAppWithSecurityReport;
showContractList: (id: string, type: ContractListTypes, hasPreviousStep: boolean) => void;
} }
const MarketplaceAppModal = ({ const MarketplaceAppModal = ({
...@@ -25,8 +31,13 @@ const MarketplaceAppModal = ({ ...@@ -25,8 +31,13 @@ const MarketplaceAppModal = ({
isFavorite, isFavorite,
onFavoriteClick, onFavoriteClick,
data, data,
showContractList: showContractListProp,
}: Props) => { }: Props) => {
const { value: isExperiment } = useFeatureValue('security_score_exp', false);
const starOutlineIconColor = useColorModeValue('gray.600', 'gray.300');
const { const {
id,
title, title,
url, url,
external, external,
...@@ -39,6 +50,7 @@ const MarketplaceAppModal = ({ ...@@ -39,6 +50,7 @@ const MarketplaceAppModal = ({
logo, logo,
logoDarkMode, logoDarkMode,
categories, categories,
securityReport,
} = data; } = data;
const socialLinks = [ const socialLinks = [
...@@ -61,8 +73,27 @@ const MarketplaceAppModal = ({ ...@@ -61,8 +73,27 @@ const MarketplaceAppModal = ({
} }
const handleFavoriteClick = useCallback(() => { const handleFavoriteClick = useCallback(() => {
onFavoriteClick(data.id, isFavorite); onFavoriteClick(id, isFavorite, 'App modal');
}, [ onFavoriteClick, data.id, isFavorite ]); }, [ onFavoriteClick, id, isFavorite ]);
const showContractList = useCallback((type: ContractListTypes) => {
onClose();
showContractListProp(id, type, true);
}, [ onClose, showContractListProp, id ]);
const showAllContracts = React.useCallback(() => {
mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'Total contracts', Info: id, Source: 'App modal' });
showContractList(ContractListTypes.ALL);
}, [ showContractList, id ]);
const showVerifiedContracts = React.useCallback(() => {
mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'Verified contracts', Info: id, Source: 'App modal' });
showContractList(ContractListTypes.VERIFIED);
}, [ showContractList, id ]);
const showAnalyzedContracts = React.useCallback(() => {
showContractList(ContractListTypes.ANALYZED);
}, [ showContractList ]);
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const logoUrl = useColorModeValue(logo, logoDarkMode || logo); const logoUrl = useColorModeValue(logo, logoDarkMode || logo);
...@@ -77,10 +108,11 @@ const MarketplaceAppModal = ({ ...@@ -77,10 +108,11 @@ const MarketplaceAppModal = ({
<ModalOverlay/> <ModalOverlay/>
<ModalContent> <ModalContent>
<ModalHeader <Box
display="grid" display="grid"
gridTemplateColumns={{ base: 'auto 1fr' }} gridTemplateColumns={{ base: 'auto 1fr' }}
paddingRight={{ sm: 12 }} paddingRight={{ sm: 12 }}
marginBottom={{ base: 6, sm: 8 }}
> >
<Flex <Flex
alignItems="center" alignItems="center"
...@@ -121,29 +153,54 @@ const MarketplaceAppModal = ({ ...@@ -121,29 +153,54 @@ const MarketplaceAppModal = ({
gridColumn={{ base: '1 / 3', sm: 2 }} gridColumn={{ base: '1 / 3', sm: 2 }}
marginTop={{ base: 6, sm: 0 }} marginTop={{ base: 6, sm: 0 }}
> >
<Box display="flex"> <Flex flexWrap="wrap" gap={ 6 }>
<MarketplaceAppModalLink <Flex width={{ base: '100%', sm: 'auto' }}>
id={ data.id } <MarketplaceAppModalLink
url={ url } id={ data.id }
external={ external } url={ url }
title={ title } external={ external }
/> title={ title }
/>
<IconButton <IconButton
aria-label="Mark as favorite" aria-label="Mark as favorite"
title="Mark as favorite" title="Mark as favorite"
variant="outline" variant="outline"
colorScheme="gray" colorScheme="gray"
w={ 9 } w={ 9 }
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={ starOutlineIconColor }/> }
/> />
</Box> </Flex>
{ (isExperiment && securityReport) && (
<Flex alignItems="center" gap={ 3 }>
<AppSecurityReport
id={ id }
securityReport={ securityReport }
showContractList={ showAnalyzedContracts }
source="App modal"
/>
<ContractListButton
onClick={ showAllContracts }
variant={ ContractListButtonVariants.ALL_CONTRACTS }
>
{ securityReport.overallInfo.totalContractsNumber }
</ContractListButton>
<ContractListButton
onClick={ showVerifiedContracts }
variant={ ContractListButtonVariants.VERIFIED_CONTRACTS }
>
{ securityReport.overallInfo.verifiedNumber }
</ContractListButton>
</Flex>
) }
</Flex>
</Box> </Box>
</ModalHeader> </Box>
<ModalCloseButton/> <ModalCloseButton/>
......
import { chakra, Flex, Tooltip, Skeleton } from '@chakra-ui/react'; import { chakra, Flex, Tooltip, Skeleton, useBoolean, Box } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { MarketplaceAppOverview } from 'types/client/marketplace'; import type { MarketplaceAppOverview, MarketplaceAppSecurityReport } from 'types/client/marketplace';
import { ContractListTypes } from 'types/client/marketplace';
import { route } from 'nextjs-routes'; import { route } from 'nextjs-routes';
import { useAppContext } from 'lib/contexts/app'; import { useAppContext } from 'lib/contexts/app';
import useFeatureValue from 'lib/growthbook/useFeatureValue';
import useIsMobile from 'lib/hooks/useIsMobile';
import IconSvg from 'ui/shared/IconSvg'; import IconSvg from 'ui/shared/IconSvg';
import LinkExternal from 'ui/shared/LinkExternal'; import LinkExternal from 'ui/shared/LinkExternal';
import LinkInternal from 'ui/shared/LinkInternal'; import LinkInternal from 'ui/shared/LinkInternal';
import AppSecurityReport from './AppSecurityReport';
import ContractListModal from './ContractListModal';
import MarketplaceAppAlert from './MarketplaceAppAlert'; import MarketplaceAppAlert from './MarketplaceAppAlert';
import MarketplaceAppInfo from './MarketplaceAppInfo'; import MarketplaceAppInfo from './MarketplaceAppInfo';
...@@ -17,10 +22,14 @@ type Props = { ...@@ -17,10 +22,14 @@ type Props = {
data: MarketplaceAppOverview | undefined; data: MarketplaceAppOverview | undefined;
isLoading: boolean; isLoading: boolean;
isWalletConnected: boolean; isWalletConnected: boolean;
securityReport?: MarketplaceAppSecurityReport;
} }
const MarketplaceAppTopBar = ({ data, isLoading, isWalletConnected }: Props) => { const MarketplaceAppTopBar = ({ data, isLoading, isWalletConnected, securityReport }: Props) => {
const [ showContractList, setShowContractList ] = useBoolean(false);
const appProps = useAppContext(); const appProps = useAppContext();
const isMobile = useIsMobile();
const { value: isExperiment } = useFeatureValue('security_score_exp', false);
const goBackUrl = React.useMemo(() => { const goBackUrl = React.useMemo(() => {
if (appProps.referrer && appProps.referrer.includes('/apps') && !appProps.referrer.includes('/apps/')) { if (appProps.referrer && appProps.referrer.includes('/apps') && !appProps.referrer.includes('/apps/')) {
...@@ -36,34 +45,55 @@ const MarketplaceAppTopBar = ({ data, isLoading, isWalletConnected }: Props) => ...@@ -36,34 +45,55 @@ const MarketplaceAppTopBar = ({ data, isLoading, isWalletConnected }: Props) =>
} }
return ( return (
<Flex alignItems="center" flexWrap="wrap" mb={{ base: 6, md: 2 }} rowGap={ 3 } columnGap={ 2 }> <>
<Tooltip label="Back to dApps list" order={ 1 }> <Flex alignItems="center" flexWrap="wrap" mb={{ base: 6, md: 2 }} rowGap={ 3 } columnGap={ 2 }>
<LinkInternal display="inline-flex" href={ goBackUrl } h="32px" isLoading={ isLoading }> <Tooltip label="Back to dApps list" order={ 1 }>
<IconSvg name="arrows/east" boxSize={ 6 } transform="rotate(180deg)" margin="auto" color="gray.400"/> <LinkInternal display="inline-flex" href={ goBackUrl } h="32px" isLoading={ isLoading }>
</LinkInternal> <IconSvg name="arrows/east" boxSize={ 6 } transform="rotate(180deg)" margin="auto" color="gray.400"/>
</Tooltip> </LinkInternal>
<Skeleton width={{ base: '100%', md: 'auto' }} order={{ base: 4, md: 2 }} isLoaded={ !isLoading }> </Tooltip>
<MarketplaceAppAlert internalWallet={ data?.internalWallet } isWalletConnected={ isWalletConnected }/> <Skeleton width={{ base: '100%', md: 'auto' }} order={{ base: 5, md: 2 }} isLoaded={ !isLoading }>
</Skeleton> <MarketplaceAppAlert internalWallet={ data?.internalWallet } isWalletConnected={ isWalletConnected }/>
<Skeleton order={{ base: 2, md: 3 }} isLoaded={ !isLoading }> </Skeleton>
<MarketplaceAppInfo data={ data }/> <Skeleton order={{ base: 2, md: 3 }} isLoaded={ !isLoading }>
</Skeleton> <MarketplaceAppInfo data={ data }/>
<LinkExternal </Skeleton>
order={{ base: 3, md: 4 }} { (isExperiment && (securityReport || isLoading)) && (
href={ data?.url } <Box order={{ base: 3, md: 4 }}>
variant="subtle" <AppSecurityReport
fontSize="sm" id={ data?.id || '' }
lineHeight={ 5 } securityReport={ securityReport }
minW={ 0 } showContractList={ setShowContractList.on }
maxW={{ base: 'calc(100% - 114px)', md: 'auto' }} isLoading={ isLoading }
display="flex" onlyIcon={ isMobile }
isLoading={ isLoading } source="App page"
> />
<chakra.span isTruncated> </Box>
{ getHostname(data?.url) } ) }
</chakra.span> <LinkExternal
</LinkExternal> order={{ base: 4, md: 5 }}
</Flex> href={ data?.url }
variant="subtle"
fontSize="sm"
lineHeight={ 5 }
minW={ 0 }
maxW={{ base: 'calc(100% - 114px)', md: 'auto' }}
display="flex"
isLoading={ isLoading }
>
<chakra.span isTruncated>
{ getHostname(data?.url) }
</chakra.span>
</LinkExternal>
</Flex>
{ showContractList && (
<ContractListModal
type={ ContractListTypes.ANALYZED }
contracts={ securityReport?.contractsData }
onClose={ setShowContractList.off }
/>
) }
</>
); );
}; };
......
import { Grid } from '@chakra-ui/react'; import { Grid } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
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 { apos } from 'lib/html-entities';
import EmptySearchResult from 'ui/shared/EmptySearchResult';
import IconSvg from 'ui/shared/IconSvg';
import EmptySearchResult from './EmptySearchResult';
import MarketplaceAppCard from './MarketplaceAppCard'; import MarketplaceAppCard from './MarketplaceAppCard';
type Props = { type Props = {
apps: Array<MarketplaceAppPreview>; apps: Array<MarketplaceAppPreview>;
onAppClick: (id: string) => void; showAppInfo: (id: string) => void;
favoriteApps: Array<string>; favoriteApps: Array<string>;
onFavoriteClick: (id: string, isFavorite: boolean) => void; onFavoriteClick: (id: string, isFavorite: boolean, source: 'Discovery view') => void;
isLoading: boolean; isLoading: boolean;
showDisclaimer: (id: string) => void;
selectedCategoryId?: string; selectedCategoryId?: string;
onAppClick: (event: MouseEvent, id: string) => void;
} }
const MarketplaceList = ({ apps, onAppClick, favoriteApps, onFavoriteClick, isLoading, showDisclaimer, selectedCategoryId }: Props) => { const MarketplaceList = ({ apps, showAppInfo, favoriteApps, onFavoriteClick, isLoading, selectedCategoryId, onAppClick }: Props) => {
return apps.length > 0 ? ( return apps.length > 0 ? (
<Grid <Grid
templateColumns={{ templateColumns={{
...@@ -33,7 +30,7 @@ const MarketplaceList = ({ apps, onAppClick, favoriteApps, onFavoriteClick, isLo ...@@ -33,7 +30,7 @@ const MarketplaceList = ({ apps, onAppClick, favoriteApps, onFavoriteClick, isLo
{ apps.map((app, index) => ( { apps.map((app, index) => (
<MarketplaceAppCard <MarketplaceAppCard
key={ app.id + (isLoading ? index : '') } key={ app.id + (isLoading ? index : '') }
onInfoClick={ onAppClick } onInfoClick={ showAppInfo }
id={ app.id } id={ app.id }
external={ app.external } external={ app.external }
url={ app.url } url={ app.url }
...@@ -45,24 +42,13 @@ const MarketplaceList = ({ apps, onAppClick, favoriteApps, onFavoriteClick, isLo ...@@ -45,24 +42,13 @@ const MarketplaceList = ({ apps, onAppClick, favoriteApps, onFavoriteClick, isLo
isFavorite={ favoriteApps.includes(app.id) } isFavorite={ favoriteApps.includes(app.id) }
onFavoriteClick={ onFavoriteClick } onFavoriteClick={ onFavoriteClick }
isLoading={ isLoading } isLoading={ isLoading }
showDisclaimer={ showDisclaimer }
internalWallet={ app.internalWallet } internalWallet={ app.internalWallet }
onAppClick={ onAppClick }
/> />
)) } )) }
</Grid> </Grid>
) : ( ) : (
<EmptySearchResult <EmptySearchResult selectedCategoryId={ selectedCategoryId } favoriteApps={ favoriteApps }/>
text={
(selectedCategoryId === MarketplaceCategory.FAVORITES && !favoriteApps.length) ? (
<>
You don{ apos }t have any favorite apps.
Click on the <IconSvg name="star_outline" w={ 4 } h={ 4 } mb={ -0.5 }/> icon on the app{ apos }s card to add it to Favorites.
</>
) : (
`Couldn${ apos }t find an app that matches your filter query.`
)
}
/>
); );
}; };
......
import { Hide, Show } from '@chakra-ui/react';
import React from 'react';
import type { MouseEvent } from 'react';
import type { MarketplaceAppWithSecurityReport, ContractListTypes } from 'types/client/marketplace';
import DataListDisplay from 'ui/shared/DataListDisplay';
import EmptySearchResult from './EmptySearchResult';
import ListItem from './MarketplaceListWithScores/ListItem';
import Table from './MarketplaceListWithScores/Table';
interface Props {
apps: Array<MarketplaceAppWithSecurityReport>;
showAppInfo: (id: string) => void;
favoriteApps: Array<string>;
onFavoriteClick: (id: string, isFavorite: boolean, source: 'Security view') => void;
isLoading: boolean;
selectedCategoryId?: string;
onAppClick: (event: MouseEvent, id: string) => void;
showContractList: (id: string, type: ContractListTypes) => void;
}
const MarketplaceListWithScores = ({
apps,
showAppInfo,
favoriteApps,
onFavoriteClick,
isLoading,
selectedCategoryId,
onAppClick,
showContractList,
}: Props) => {
const displayedApps = React.useMemo(() => apps.sort((a, b) => {
if (!a.securityReport) {
return 1;
} else if (!b.securityReport) {
return -1;
}
return b.securityReport.overallInfo.securityScore - a.securityReport.overallInfo.securityScore;
}), [ apps ]);
const content = apps.length > 0 ? (
<>
<Show below="lg" ssr={ false }>
{ displayedApps.map((app, index) => (
<ListItem
key={ app.id + (isLoading ? index : '') }
app={ app }
onInfoClick={ showAppInfo }
isFavorite={ favoriteApps.includes(app.id) }
onFavoriteClick={ onFavoriteClick }
isLoading={ isLoading }
onAppClick={ onAppClick }
showContractList={ showContractList }
/>
)) }
</Show>
<Hide below="lg" ssr={ false }>
<Table
apps={ displayedApps }
isLoading={ isLoading }
onAppClick={ onAppClick }
favoriteApps={ favoriteApps }
onFavoriteClick={ onFavoriteClick }
onInfoClick={ showAppInfo }
showContractList={ showContractList }
/>
</Hide>
</>
) : null;
return apps.length > 0 ? (
<DataListDisplay
isError={ false }
items={ apps }
emptyText="No apps found."
content={ content }
/>
) : (
<EmptySearchResult selectedCategoryId={ selectedCategoryId } favoriteApps={ favoriteApps }/>
);
};
export default MarketplaceListWithScores;
import { Flex, Skeleton, LinkBox, Image, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
import type { MouseEvent } from 'react';
import type { MarketplaceAppPreview } from 'types/client/marketplace';
import MarketplaceAppCardLink from '../MarketplaceAppCardLink';
import MarketplaceAppIntegrationIcon from '../MarketplaceAppIntegrationIcon';
interface Props {
app: MarketplaceAppPreview;
isLoading: boolean | undefined;
onAppClick: (event: MouseEvent, id: string) => void;
isLarge?: boolean;
}
const AppLink = ({ app, isLoading, onAppClick, isLarge = false }: Props) => {
const { id, url, external, title, logo, logoDarkMode, internalWallet, categories } = app;
const logoUrl = useColorModeValue(logo, logoDarkMode || logo);
const categoriesLabel = categories.join(', ');
return (
<LinkBox display="flex" height="100%" width="100%" role="group" alignItems="center" mb={ isLarge ? 0 : 4 }>
<Skeleton
isLoaded={ !isLoading }
w={ isLarge ? '56px' : '48px' }
h={ isLarge ? '56px' : '48px' }
display="flex"
alignItems="center"
justifyContent="center"
mr={ isLarge ? 3 : 4 }
>
<Image
src={ isLoading ? undefined : logoUrl }
alt={ `${ title } app icon` }
borderRadius="8px"
/>
</Skeleton>
<Flex direction="column">
<Skeleton
isLoaded={ !isLoading }
marginBottom={ 0 }
fontSize="sm"
fontWeight="semibold"
fontFamily="heading"
display="inline-block"
mb={ 1 }
>
<MarketplaceAppCardLink
id={ id }
url={ url }
external={ external }
title={ title }
onClick={ onAppClick }
/>
<MarketplaceAppIntegrationIcon external={ external } internalWallet={ internalWallet }/>
</Skeleton>
<Skeleton
isLoaded={ !isLoading }
color="text_secondary"
fontSize={ isLarge ? 'sm' : 'xs' }
>
<span>{ categoriesLabel }</span>
</Skeleton>
</Flex>
</LinkBox>
);
};
export default AppLink;
import { Flex, IconButton, Text } from '@chakra-ui/react';
import React from 'react';
import type { MouseEvent } from 'react';
import type { MarketplaceAppWithSecurityReport } from 'types/client/marketplace';
import { ContractListTypes } from 'types/client/marketplace';
import * as mixpanel from 'lib/mixpanel/index';
import IconSvg from 'ui/shared/IconSvg';
import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
import AppSecurityReport from '../AppSecurityReport';
import ContractListButton, { ContractListButtonVariants } from '../ContractListButton';
import AppLink from './AppLink';
import MoreInfoButton from './MoreInfoButton';
type Props = {
app: MarketplaceAppWithSecurityReport;
onInfoClick: (id: string) => void;
isFavorite: boolean;
onFavoriteClick: (id: string, isFavorite: boolean, source: 'Security view') => void;
isLoading: boolean;
onAppClick: (event: MouseEvent, id: string) => void;
showContractList: (id: string, type: ContractListTypes) => void;
}
const ListItem = ({ app, onInfoClick, isFavorite, onFavoriteClick, isLoading, onAppClick, showContractList }: Props) => {
const { id, securityReport } = app;
const handleInfoClick = React.useCallback((event: MouseEvent) => {
event.preventDefault();
mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'More button', Info: id, Source: 'Security view' });
onInfoClick(id);
}, [ onInfoClick, id ]);
const handleFavoriteClick = React.useCallback(() => {
onFavoriteClick(id, isFavorite, 'Security view');
}, [ onFavoriteClick, id, isFavorite ]);
const showAllContracts = React.useCallback(() => {
mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'Total contracts', Info: id, Source: 'Security view' });
showContractList(id, ContractListTypes.ALL);
}, [ showContractList, id ]);
const showVerifiedContracts = React.useCallback(() => {
mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'Verified contracts', Info: id, Source: 'Security view' });
showContractList(id, ContractListTypes.VERIFIED);
}, [ showContractList, id ]);
const showAnalyzedContracts = React.useCallback(() => {
showContractList(id, ContractListTypes.ANALYZED);
}, [ showContractList, id ]);
return (
<ListItemMobile
rowGap={ 3 }
py={ 3 }
_first={{ borderTop: 'none', paddingTop: 0 }}
_last={{ borderBottom: 'none', paddingBottom: 0 }}
>
<Flex
direction="column"
justifyContent="stretch"
padding={ 3 }
width="100%"
>
<Flex position="relative">
<AppLink app={ app } isLoading={ isLoading } onAppClick={ onAppClick }/>
{ !isLoading && (
<IconButton
position="absolute"
right={ -1 }
top={ -1 }
aria-label="Mark as favorite"
title="Mark as favorite"
variant="ghost"
colorScheme="gray"
w={ 9 }
h={ 8 }
onClick={ handleFavoriteClick }
icon={ isFavorite ?
<IconSvg name="star_filled" w={ 5 } h={ 5 } color="yellow.400"/> :
<IconSvg name="star_outline" w={ 5 } h={ 5 } color="gray.400"/>
}
/>
) }
</Flex>
<Flex alignItems="center">
<Flex flex={ 1 } gap={ 3 } alignItems="center">
{ (securityReport || isLoading) ? (
<>
<AppSecurityReport
id={ id }
isLoading={ isLoading }
securityReport={ securityReport }
showContractList={ showAnalyzedContracts }
height="30px"
source="Security view"
/>
<ContractListButton
onClick={ showAllContracts }
variant={ ContractListButtonVariants.ALL_CONTRACTS }
isLoading={ isLoading }
>
{ securityReport?.overallInfo.totalContractsNumber ?? 0 }
</ContractListButton>
<ContractListButton
onClick={ showVerifiedContracts }
variant={ ContractListButtonVariants.VERIFIED_CONTRACTS }
isLoading={ isLoading }
>
{ securityReport?.overallInfo.verifiedNumber ?? 0 }
</ContractListButton>
</>
) : (
<Text variant="secondary" fontSize="sm" fontWeight={ 500 }>Data will be available soon</Text>
) }
</Flex>
<MoreInfoButton onClick={ handleInfoClick } isLoading={ isLoading }/>
</Flex>
</Flex>
</ListItemMobile>
);
};
export default ListItem;
import { Link, Skeleton } from '@chakra-ui/react';
import React from 'react';
import type { MouseEvent } from 'react';
interface Props {
onClick: (event: MouseEvent) => void;
isLoading?: boolean;
}
const MoreInfoButton = ({ onClick, isLoading }: Props) => (
<Skeleton
isLoaded={ !isLoading }
display="inline-flex"
alignItems="center"
height="30px"
borderRadius="base"
>
<Link
fontSize="sm"
onClick={ onClick }
fontWeight="500"
display="inline-flex"
>
More info
</Link>
</Skeleton>
);
export default MoreInfoButton;
import { Table as ChakraTable, Tbody, Th, Tr } from '@chakra-ui/react';
import React from 'react';
import type { MouseEvent } from 'react';
import type { MarketplaceAppWithSecurityReport, ContractListTypes } from 'types/client/marketplace';
import { default as Thead } from 'ui/shared/TheadSticky';
import TableItem from './TableItem';
type Props = {
apps: Array<MarketplaceAppWithSecurityReport>;
isLoading?: boolean;
favoriteApps: Array<string>;
onFavoriteClick: (id: string, isFavorite: boolean, source: 'Security view') => 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, showContractList }: Props) => {
return (
<ChakraTable>
<Thead top={ 0 }>
<Tr>
<Th w="5%"></Th>
<Th w="40%">App</Th>
<Th w="15%">Contracts score</Th>
<Th w="10%">Total</Th>
<Th w="10%">Verified</Th>
<Th w="20%"></Th>
</Tr>
</Thead>
<Tbody>
{ apps.map((app, index) => (
<TableItem
key={ app.id + (isLoading ? index : '') }
app={ app }
isLoading={ isLoading }
isFavorite={ favoriteApps.includes(app.id) }
onFavoriteClick={ onFavoriteClick }
onAppClick={ onAppClick }
onInfoClick={ onInfoClick }
showContractList={ showContractList }
/>
)) }
</Tbody>
</ChakraTable>
);
};
export default Table;
import { Td, Tr, IconButton, Skeleton, Text } from '@chakra-ui/react';
import React from 'react';
import type { MouseEvent } from 'react';
import type { MarketplaceAppWithSecurityReport } from 'types/client/marketplace';
import { ContractListTypes } from 'types/client/marketplace';
import * as mixpanel from 'lib/mixpanel/index';
import IconSvg from 'ui/shared/IconSvg';
import AppSecurityReport from '../AppSecurityReport';
import ContractListButton, { ContractListButtonVariants } from '../ContractListButton';
import AppLink from './AppLink';
import MoreInfoButton from './MoreInfoButton';
type Props = {
app: MarketplaceAppWithSecurityReport;
isLoading?: boolean;
isFavorite: boolean;
onFavoriteClick: (id: string, isFavorite: boolean, source: 'Security view') => void;
onAppClick: (event: MouseEvent, id: string) => void;
onInfoClick: (id: string) => void;
showContractList: (id: string, type: ContractListTypes) => void;
}
const TableItem = ({
app,
isLoading,
isFavorite,
onFavoriteClick,
onAppClick,
onInfoClick,
showContractList,
}: Props) => {
const { id, securityReport } = app;
const handleInfoClick = React.useCallback((event: MouseEvent) => {
event.preventDefault();
mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'More button', Info: id, Source: 'Security view' });
onInfoClick(id);
}, [ onInfoClick, id ]);
const handleFavoriteClick = React.useCallback(() => {
onFavoriteClick(id, isFavorite, 'Security view');
}, [ onFavoriteClick, id, isFavorite ]);
const showAllContracts = React.useCallback(() => {
mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'Total contracts', Info: id, Source: 'Security view' });
showContractList(id, ContractListTypes.ALL);
}, [ showContractList, id ]);
const showVerifiedContracts = React.useCallback(() => {
mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'Verified contracts', Info: id, Source: 'Security view' });
showContractList(id, ContractListTypes.VERIFIED);
}, [ showContractList, id ]);
const showAnalyzedContracts = React.useCallback(() => {
showContractList(id, ContractListTypes.ANALYZED);
}, [ showContractList, id ]);
return (
<Tr>
<Td verticalAlign="middle" px={ 2 }>
<Skeleton isLoaded={ !isLoading }>
<IconButton
aria-label="Mark as favorite"
title="Mark as favorite"
variant="ghost"
colorScheme="gray"
w={ 9 }
h={ 8 }
onClick={ handleFavoriteClick }
icon={ isFavorite ?
<IconSvg name="star_filled" w={ 5 } h={ 5 } color="yellow.400"/> :
<IconSvg name="star_outline" w={ 5 } h={ 5 } color="gray.400"/>
}
/>
</Skeleton>
</Td>
<Td verticalAlign="middle">
<AppLink app={ app } isLoading={ isLoading } onAppClick={ onAppClick } isLarge/>
</Td>
{ (securityReport || isLoading) ? (
<>
<Td verticalAlign="middle">
<AppSecurityReport
id={ id }
securityReport={ securityReport }
showContractList={ showAnalyzedContracts }
isLoading={ isLoading }
source="Security view"
/>
</Td>
<Td verticalAlign="middle">
<ContractListButton
onClick={ showAllContracts }
variant={ ContractListButtonVariants.ALL_CONTRACTS }
isLoading={ isLoading }
>
{ securityReport?.overallInfo.totalContractsNumber ?? 0 }
</ContractListButton>
</Td>
<Td verticalAlign="middle">
<ContractListButton
onClick={ showVerifiedContracts }
variant={ ContractListButtonVariants.VERIFIED_CONTRACTS }
isLoading={ isLoading }
>
{ securityReport?.overallInfo.verifiedNumber ?? 0 }
</ContractListButton>
</Td>
</>
) : (
<Td verticalAlign="middle" colSpan={ 3 }>
<Text variant="secondary" fontSize="sm" fontWeight={ 500 }>Data will be available soon</Text>
</Td>
) }
<Td verticalAlign="middle" isNumeric>
<MoreInfoButton onClick={ handleInfoClick } isLoading={ isLoading }/>
</Td>
</Tr>
);
};
export default TableItem;
...@@ -2,7 +2,8 @@ import _pickBy from 'lodash/pickBy'; ...@@ -2,7 +2,8 @@ import _pickBy from 'lodash/pickBy';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import { MarketplaceCategory } from 'types/client/marketplace'; import type { ContractListTypes } from 'types/client/marketplace';
import { MarketplaceCategory, MarketplaceDisplayType } from 'types/client/marketplace';
import useDebounce from 'lib/hooks/useDebounce'; import useDebounce from 'lib/hooks/useDebounce';
import * as mixpanel from 'lib/mixpanel/index'; import * as mixpanel from 'lib/mixpanel/index';
...@@ -25,17 +26,25 @@ export default function useMarketplace() { ...@@ -25,17 +26,25 @@ export default function useMarketplace() {
const router = useRouter(); const router = useRouter();
const defaultCategoryId = getQueryParamString(router.query.category); const defaultCategoryId = getQueryParamString(router.query.category);
const defaultFilterQuery = getQueryParamString(router.query.filter); const defaultFilterQuery = getQueryParamString(router.query.filter);
const defaultDisplayType = getQueryParamString(router.query.tab);
const [ selectedAppId, setSelectedAppId ] = React.useState<string | null>(null); const [ selectedAppId, setSelectedAppId ] = React.useState<string | null>(null);
const [ selectedCategoryId, setSelectedCategoryId ] = React.useState<string>(MarketplaceCategory.ALL); const [ selectedCategoryId, setSelectedCategoryId ] = React.useState<string>(MarketplaceCategory.ALL);
const [ selectedDisplayType, setSelectedDisplayType ] = React.useState<string>(
Object.values(MarketplaceDisplayType).includes(defaultDisplayType as MarketplaceDisplayType) ?
defaultDisplayType :
MarketplaceDisplayType.DEFAULT,
);
const [ filterQuery, setFilterQuery ] = React.useState(defaultFilterQuery); const [ filterQuery, setFilterQuery ] = React.useState(defaultFilterQuery);
const [ favoriteApps, setFavoriteApps ] = React.useState<Array<string>>([]); const [ favoriteApps, setFavoriteApps ] = React.useState<Array<string>>([]);
const [ isFavoriteAppsLoaded, setIsFavoriteAppsLoaded ] = React.useState<boolean>(false); const [ isFavoriteAppsLoaded, setIsFavoriteAppsLoaded ] = React.useState<boolean>(false);
const [ isAppInfoModalOpen, setIsAppInfoModalOpen ] = React.useState<boolean>(false); const [ isAppInfoModalOpen, setIsAppInfoModalOpen ] = React.useState<boolean>(false);
const [ isDisclaimerModalOpen, setIsDisclaimerModalOpen ] = React.useState<boolean>(false); const [ isDisclaimerModalOpen, setIsDisclaimerModalOpen ] = React.useState<boolean>(false);
const [ contractListModalType, setContractListModalType ] = React.useState<ContractListTypes | null>(null);
const [ hasPreviousStep, setHasPreviousStep ] = React.useState<boolean>(false);
const handleFavoriteClick = React.useCallback((id: string, isFavorite: boolean) => { const handleFavoriteClick = React.useCallback((id: string, isFavorite: boolean, source: 'Discovery view' | 'Security view' | 'App modal') => {
mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'Favorite app', Info: id }); mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'Favorite app', Info: id, Source: source });
const favoriteApps = getFavoriteApps(); const favoriteApps = getFavoriteApps();
...@@ -60,11 +69,21 @@ export default function useMarketplace() { ...@@ -60,11 +69,21 @@ export default function useMarketplace() {
setIsDisclaimerModalOpen(true); setIsDisclaimerModalOpen(true);
}, []); }, []);
const showContractList = React.useCallback((id: string, type: ContractListTypes, hasPreviousStep?: boolean) => {
setSelectedAppId(id);
setContractListModalType(type);
if (hasPreviousStep) {
setHasPreviousStep(true);
}
}, []);
const debouncedFilterQuery = useDebounce(filterQuery, 500); const debouncedFilterQuery = useDebounce(filterQuery, 500);
const clearSelectedAppId = React.useCallback(() => { const clearSelectedAppId = React.useCallback(() => {
setSelectedAppId(null); setSelectedAppId(null);
setIsAppInfoModalOpen(false); setIsAppInfoModalOpen(false);
setIsDisclaimerModalOpen(false); setIsDisclaimerModalOpen(false);
setContractListModalType(null);
setHasPreviousStep(false);
}, []); }, []);
const handleCategoryChange = React.useCallback((newCategory: string) => { const handleCategoryChange = React.useCallback((newCategory: string) => {
...@@ -72,6 +91,10 @@ export default function useMarketplace() { ...@@ -72,6 +91,10 @@ export default function useMarketplace() {
setSelectedCategoryId(newCategory); setSelectedCategoryId(newCategory);
}, []); }, []);
const handleDisplayTypeChange = React.useCallback((newDisplayType: MarketplaceDisplayType) => {
setSelectedDisplayType(newDisplayType);
}, []);
const { const {
isPlaceholderData, isError, error, data, displayedApps, isPlaceholderData, isError, error, data, displayedApps,
} = useMarketplaceApps(debouncedFilterQuery, selectedCategoryId, favoriteApps, isFavoriteAppsLoaded); } = useMarketplaceApps(debouncedFilterQuery, selectedCategoryId, favoriteApps, isFavoriteAppsLoaded);
...@@ -97,6 +120,7 @@ export default function useMarketplace() { ...@@ -97,6 +120,7 @@ export default function useMarketplace() {
const query = _pickBy({ const query = _pickBy({
category: selectedCategoryId === MarketplaceCategory.ALL ? undefined : selectedCategoryId, category: selectedCategoryId === MarketplaceCategory.ALL ? undefined : selectedCategoryId,
filter: debouncedFilterQuery, filter: debouncedFilterQuery,
tab: selectedDisplayType === MarketplaceDisplayType.DEFAULT ? undefined : selectedDisplayType,
}, Boolean); }, Boolean);
if (debouncedFilterQuery.length > 0) { if (debouncedFilterQuery.length > 0) {
...@@ -111,7 +135,7 @@ export default function useMarketplace() { ...@@ -111,7 +135,7 @@ export default function useMarketplace() {
// omit router in the deps because router.push() somehow modifies it // omit router in the deps because router.push() somehow modifies it
// and we get infinite re-renders then // and we get infinite re-renders then
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [ debouncedFilterQuery, selectedCategoryId ]); }, [ debouncedFilterQuery, selectedCategoryId, selectedDisplayType ]);
return React.useMemo(() => ({ return React.useMemo(() => ({
selectedCategoryId, selectedCategoryId,
...@@ -133,6 +157,11 @@ export default function useMarketplace() { ...@@ -133,6 +157,11 @@ export default function useMarketplace() {
showDisclaimer, showDisclaimer,
appsTotal: data?.length || 0, appsTotal: data?.length || 0,
isCategoriesPlaceholderData, isCategoriesPlaceholderData,
showContractList,
contractListModalType,
selectedDisplayType,
onDisplayTypeChange: handleDisplayTypeChange,
hasPreviousStep,
}), [ }), [
selectedCategoryId, selectedCategoryId,
categories, categories,
...@@ -152,5 +181,10 @@ export default function useMarketplace() { ...@@ -152,5 +181,10 @@ export default function useMarketplace() {
showDisclaimer, showDisclaimer,
data?.length, data?.length,
isCategoriesPlaceholderData, isCategoriesPlaceholderData,
showContractList,
contractListModalType,
selectedDisplayType,
handleDisplayTypeChange,
hasPreviousStep,
]); ]);
} }
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import React from 'react'; import React from 'react';
import type { MarketplaceAppOverview } from 'types/client/marketplace'; import type { MarketplaceAppWithSecurityReport } from 'types/client/marketplace';
import { MarketplaceCategory } from 'types/client/marketplace'; import { MarketplaceCategory } from 'types/client/marketplace';
import config from 'configs/app'; import config from 'configs/app';
...@@ -10,19 +10,21 @@ import useApiFetch from 'lib/api/useApiFetch'; ...@@ -10,19 +10,21 @@ import useApiFetch from 'lib/api/useApiFetch';
import useFetch from 'lib/hooks/useFetch'; import useFetch from 'lib/hooks/useFetch';
import { MARKETPLACE_APP } from 'stubs/marketplace'; import { MARKETPLACE_APP } from 'stubs/marketplace';
import useSecurityReports from './useSecurityReports';
const feature = config.features.marketplace; const feature = config.features.marketplace;
function isAppNameMatches(q: string, app: MarketplaceAppOverview) { function isAppNameMatches(q: string, app: MarketplaceAppWithSecurityReport) {
return app.title.toLowerCase().includes(q.toLowerCase()); return app.title.toLowerCase().includes(q.toLowerCase());
} }
function isAppCategoryMatches(category: string, app: MarketplaceAppOverview, favoriteApps: Array<string>) { function isAppCategoryMatches(category: string, app: MarketplaceAppWithSecurityReport, favoriteApps: Array<string>) {
return category === MarketplaceCategory.ALL || return category === MarketplaceCategory.ALL ||
(category === MarketplaceCategory.FAVORITES && favoriteApps.includes(app.id)) || (category === MarketplaceCategory.FAVORITES && favoriteApps.includes(app.id)) ||
app.categories.includes(category); app.categories.includes(category);
} }
function sortApps(apps: Array<MarketplaceAppOverview>, favoriteApps: Array<string>) { function sortApps(apps: Array<MarketplaceAppWithSecurityReport>, favoriteApps: Array<string>) {
return apps.sort((a, b) => { return apps.sort((a, b) => {
const priorityA = a.priority || 0; const priorityA = a.priority || 0;
const priorityB = b.priority || 0; const priorityB = b.priority || 0;
...@@ -56,6 +58,8 @@ export default function useMarketplaceApps( ...@@ -56,6 +58,8 @@ export default function useMarketplaceApps(
const fetch = useFetch(); const fetch = useFetch();
const apiFetch = useApiFetch(); const apiFetch = useApiFetch();
const { data: securityReports, isPlaceholderData: isSecurityReportsPlaceholderData } = useSecurityReports();
// Update favorite apps only when selectedCategoryId changes to avoid sortApps to be called on each favorite app click // Update favorite apps only when selectedCategoryId changes to avoid sortApps to be called on each favorite app click
const [ snapshotFavoriteApps, setSnapshotFavoriteApps ] = React.useState<Array<string> | undefined>(); const [ snapshotFavoriteApps, setSnapshotFavoriteApps ] = React.useState<Array<string> | undefined>();
...@@ -65,38 +69,43 @@ export default function useMarketplaceApps( ...@@ -65,38 +69,43 @@ export default function useMarketplaceApps(
} }
}, [ selectedCategoryId, isFavoriteAppsLoaded ]); // eslint-disable-line react-hooks/exhaustive-deps }, [ selectedCategoryId, isFavoriteAppsLoaded ]); // eslint-disable-line react-hooks/exhaustive-deps
const { isPlaceholderData, isError, error, data } = useQuery<unknown, ResourceError<unknown>, Array<MarketplaceAppOverview>>({ const { isPlaceholderData, isError, error, data } = useQuery<unknown, ResourceError<unknown>, Array<MarketplaceAppWithSecurityReport>>({
queryKey: [ 'marketplace-dapps', snapshotFavoriteApps, favoriteApps ], queryKey: [ 'marketplace-dapps', snapshotFavoriteApps, favoriteApps ],
queryFn: async() => { queryFn: async() => {
if (!feature.isEnabled) { if (!feature.isEnabled) {
return []; return [];
} else if ('configUrl' in feature) { } else if ('configUrl' in feature) {
return fetch<Array<MarketplaceAppOverview>, unknown>(feature.configUrl, undefined, { resource: 'marketplace-dapps' }); return fetch<Array<MarketplaceAppWithSecurityReport>, unknown>(feature.configUrl, undefined, { resource: 'marketplace-dapps' });
} else { } else {
return apiFetch('marketplace_dapps', { pathParams: { chainId: config.chain.id } }); return apiFetch('marketplace_dapps', { pathParams: { chainId: config.chain.id } });
} }
}, },
select: (data) => sortApps(data as Array<MarketplaceAppOverview>, snapshotFavoriteApps || []), select: (data) => sortApps(data as Array<MarketplaceAppWithSecurityReport>, snapshotFavoriteApps || []),
placeholderData: feature.isEnabled ? Array(9).fill(MARKETPLACE_APP) : undefined, placeholderData: feature.isEnabled ? Array(9).fill(MARKETPLACE_APP) : undefined,
staleTime: Infinity, staleTime: Infinity,
enabled: feature.isEnabled && (!favoriteApps || Boolean(snapshotFavoriteApps)), enabled: feature.isEnabled && (!favoriteApps || Boolean(snapshotFavoriteApps)),
}); });
const appsWithSecurityReports = React.useMemo(() =>
data?.map((app) => ({ ...app, securityReport: securityReports?.[app.id] })),
[ data, securityReports ]);
const displayedApps = React.useMemo(() => { const displayedApps = React.useMemo(() => {
return data?.filter(app => isAppNameMatches(filter, app) && isAppCategoryMatches(selectedCategoryId, app, favoriteApps || [])) || []; return appsWithSecurityReports?.filter(app => isAppNameMatches(filter, app) && isAppCategoryMatches(selectedCategoryId, app, favoriteApps || [])) || [];
}, [ selectedCategoryId, data, filter, favoriteApps ]); }, [ selectedCategoryId, appsWithSecurityReports, filter, favoriteApps ]);
return React.useMemo(() => ({ return React.useMemo(() => ({
data, data,
displayedApps, displayedApps,
error, error,
isError, isError,
isPlaceholderData, isPlaceholderData: isPlaceholderData || isSecurityReportsPlaceholderData,
}), [ }), [
data, data,
displayedApps, displayedApps,
error, error,
isError, isError,
isPlaceholderData, isPlaceholderData,
isSecurityReportsPlaceholderData,
]); ]);
} }
import { useQuery } from '@tanstack/react-query';
import type { MarketplaceAppSecurityReport, MarketplaceAppSecurityReportRaw } from 'types/client/marketplace';
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>, Record<string, MarketplaceAppSecurityReport>>({
queryKey: [ 'marketplace-security-reports' ],
queryFn: async() => apiFetch(securityReportsUrl, undefined, { resource: 'marketplace-security-reports' }),
select: (data) => {
const securityReports: Record<string, MarketplaceAppSecurityReport> = {};
(data as Array<MarketplaceAppSecurityReportRaw>).forEach((item) => {
const report = item.chainsData[config.chain.id || ''];
if (report) {
const issues: Record<string, number> = report.overallInfo.issueSeverityDistribution;
report.overallInfo.totalIssues = Object.values(issues).reduce((acc, val) => acc + val, 0);
report.overallInfo.securityScore = Number(report.overallInfo.securityScore.toFixed(2));
}
securityReports[item.appName] = report;
});
return securityReports;
},
placeholderData: securityReportsUrl ? {} : undefined,
staleTime: Infinity,
enabled: Boolean(securityReportsUrl),
});
}
...@@ -3,6 +3,7 @@ import React from 'react'; ...@@ -3,6 +3,7 @@ import React from 'react';
import { buildExternalAssetFilePath } from 'configs/app/utils'; import { buildExternalAssetFilePath } from 'configs/app/utils';
import { apps as appsMock } from 'mocks/apps/apps'; import { apps as appsMock } from 'mocks/apps/apps';
import { securityReports as securityReportsMock } from 'mocks/apps/securityReports';
import contextWithEnvs from 'playwright/fixtures/contextWithEnvs'; import contextWithEnvs from 'playwright/fixtures/contextWithEnvs';
import TestApp from 'playwright/TestApp'; import TestApp from 'playwright/TestApp';
import * as app from 'playwright/utils/app'; import * as app from 'playwright/utils/app';
...@@ -10,6 +11,8 @@ import * as app from 'playwright/utils/app'; ...@@ -10,6 +11,8 @@ import * as app from 'playwright/utils/app';
import Marketplace from './Marketplace'; import Marketplace from './Marketplace';
const MARKETPLACE_CONFIG_URL = app.url + buildExternalAssetFilePath('NEXT_PUBLIC_MARKETPLACE_CONFIG_URL', 'https://marketplace-config.json') || ''; const MARKETPLACE_CONFIG_URL = app.url + buildExternalAssetFilePath('NEXT_PUBLIC_MARKETPLACE_CONFIG_URL', 'https://marketplace-config.json') || '';
const MARKETPLACE_SECURITY_REPORTS_URL =
app.url + buildExternalAssetFilePath('NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL', 'https://marketplace-security-reports.json') || '';
const test = base.extend({ const test = base.extend({
context: contextWithEnvs([ context: contextWithEnvs([
...@@ -41,3 +44,43 @@ test('base view +@mobile +@dark-mode', async({ mount, page }) => { ...@@ -41,3 +44,43 @@ test('base view +@mobile +@dark-mode', async({ mount, page }) => {
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
const testWithScoreFeature = test.extend({
context: contextWithEnvs([
{ name: 'NEXT_PUBLIC_MARKETPLACE_CONFIG_URL', value: MARKETPLACE_CONFIG_URL },
{ name: 'NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL', value: MARKETPLACE_SECURITY_REPORTS_URL },
{ name: 'pw_feature:security_score_exp', value: 'true' },
// eslint-disable-next-line @typescript-eslint/no-explicit-any
]) as any,
});
testWithScoreFeature('with scores +@mobile +@dark-mode', async({ mount, page }) => {
await page.route(MARKETPLACE_CONFIG_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(appsMock),
}));
await page.route(MARKETPLACE_SECURITY_REPORTS_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(securityReportsMock),
}));
await Promise.all(appsMock.map(app =>
page.route(app.logo, (route) =>
route.fulfill({
status: 200,
path: './playwright/mocks/image_s.jpg',
}),
),
));
const component = await mount(
<TestApp>
<Marketplace/>
</TestApp>,
);
await component.getByText('Apps scores').click();
await expect(component).toHaveScreenshot();
});
import { Box, Menu, MenuButton, MenuItem, MenuList, Flex, IconButton } from '@chakra-ui/react'; import { Box, Menu, MenuButton, MenuItem, MenuList, Flex, IconButton, Skeleton } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { MouseEvent } from 'react';
import { MarketplaceCategory } from 'types/client/marketplace'; import { MarketplaceCategory, MarketplaceDisplayType } from 'types/client/marketplace';
import type { TabItem } from 'ui/shared/Tabs/types'; import type { TabItem } from 'ui/shared/Tabs/types';
import config from 'configs/app'; import config from 'configs/app';
import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError'; import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError';
import useFeatureValue from 'lib/growthbook/useFeatureValue';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import ContractListModal from 'ui/marketplace/ContractListModal';
import MarketplaceAppModal from 'ui/marketplace/MarketplaceAppModal'; import MarketplaceAppModal from 'ui/marketplace/MarketplaceAppModal';
import MarketplaceDisclaimerModal from 'ui/marketplace/MarketplaceDisclaimerModal'; import MarketplaceDisclaimerModal from 'ui/marketplace/MarketplaceDisclaimerModal';
import MarketplaceList from 'ui/marketplace/MarketplaceList'; import MarketplaceList from 'ui/marketplace/MarketplaceList';
import MarketplaceListWithScores from 'ui/marketplace/MarketplaceListWithScores';
import FilterInput from 'ui/shared/filters/FilterInput'; import FilterInput from 'ui/shared/filters/FilterInput';
import IconSvg from 'ui/shared/IconSvg'; import IconSvg from 'ui/shared/IconSvg';
import type { IconName } from 'ui/shared/IconSvg'; import type { IconName } from 'ui/shared/IconSvg';
import LinkExternal from 'ui/shared/LinkExternal'; import LinkExternal from 'ui/shared/LinkExternal';
import PageTitle from 'ui/shared/Page/PageTitle'; import PageTitle from 'ui/shared/Page/PageTitle';
import RadioButtonGroup from 'ui/shared/radioButtonGroup/RadioButtonGroup';
import TabsSkeleton from 'ui/shared/Tabs/TabsSkeleton'; import TabsSkeleton from 'ui/shared/Tabs/TabsSkeleton';
import TabsWithScroll from 'ui/shared/Tabs/TabsWithScroll'; import TabsWithScroll from 'ui/shared/Tabs/TabsWithScroll';
...@@ -60,8 +65,15 @@ const Marketplace = () => { ...@@ -60,8 +65,15 @@ const Marketplace = () => {
showDisclaimer, showDisclaimer,
appsTotal, appsTotal,
isCategoriesPlaceholderData, isCategoriesPlaceholderData,
showContractList,
contractListModalType,
selectedDisplayType,
onDisplayTypeChange,
hasPreviousStep,
} = useMarketplace(); } = useMarketplace();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const { value: isExperiment } = useFeatureValue('security_score_exp', false);
const categoryTabs = React.useMemo(() => { const categoryTabs = React.useMemo(() => {
const tabs: Array<TabItem> = categories.map(category => ({ const tabs: Array<TabItem> = categories.map(category => ({
...@@ -80,7 +92,7 @@ const Marketplace = () => { ...@@ -80,7 +92,7 @@ const Marketplace = () => {
tabs.unshift({ tabs.unshift({
id: MarketplaceCategory.FAVORITES, id: MarketplaceCategory.FAVORITES,
title: () => <IconSvg name="star_outline" w={ 4 } h={ 4 }/>, title: () => <IconSvg name="star_outline" w={ 5 } h={ 5 }/>,
count: null, count: null,
component: null, component: null,
}); });
...@@ -93,18 +105,33 @@ const Marketplace = () => { ...@@ -93,18 +105,33 @@ const Marketplace = () => {
return index === -1 ? 0 : index; return index === -1 ? 0 : index;
}, [ categoryTabs, selectedCategoryId ]); }, [ categoryTabs, selectedCategoryId ]);
const selectedApp = displayedApps.find(app => app.id === selectedAppId);
const handleCategoryChange = React.useCallback((index: number) => { const handleCategoryChange = React.useCallback((index: number) => {
onCategoryChange(categoryTabs[index].id); onCategoryChange(categoryTabs[index].id);
}, [ categoryTabs, onCategoryChange ]); }, [ categoryTabs, onCategoryChange ]);
const handleAppClick = React.useCallback((event: MouseEvent, id: string) => {
const isShown = window.localStorage.getItem('marketplace-disclaimer-shown');
if (!isShown) {
event.preventDefault();
showDisclaimer(id);
}
}, [ showDisclaimer ]);
const handleGoBackInContractListModal = React.useCallback(() => {
clearSelectedAppId();
if (selectedApp) {
showAppInfo(selectedApp.id);
}
}, [ clearSelectedAppId, showAppInfo, selectedApp ]);
throwOnResourceLoadError(isError && error ? { isError, error } : { isError: false, error: null }); throwOnResourceLoadError(isError && error ? { isError, error } : { isError: false, error: null });
if (!feature.isEnabled) { if (!feature.isEnabled) {
return null; return null;
} }
const selectedApp = displayedApps.find(app => app.id === selectedAppId);
return ( return (
<> <>
<PageTitle <PageTitle
...@@ -148,29 +175,76 @@ const Marketplace = () => { ...@@ -148,29 +175,76 @@ const Marketplace = () => {
tabs={ categoryTabs } tabs={ categoryTabs }
onTabChange={ handleCategoryChange } onTabChange={ handleCategoryChange }
defaultTabIndex={ selectedCategoryIndex } defaultTabIndex={ selectedCategoryIndex }
marginBottom={{ base: 0, lg: -2 }} marginBottom={ -2 }
/> />
) } ) }
</Box> </Box>
<FilterInput <Flex direction={{ base: 'column', lg: 'row' }} mb={{ base: 4, lg: 6 }} gap={{ base: 4, lg: 3 }}>
initialValue={ filterQuery } { (feature.securityReportsUrl && isExperiment) && (
onChange={ onSearchInputChange } <Skeleton isLoaded={ !isPlaceholderData }>
marginBottom={{ base: '4', lg: '6' }} <RadioButtonGroup<MarketplaceDisplayType>
w="100%" onChange={ onDisplayTypeChange }
placeholder="Find app" defaultValue={ selectedDisplayType }
isLoading={ isPlaceholderData } name="type"
/> options={ [
{
title: 'Discovery',
value: MarketplaceDisplayType.DEFAULT,
icon: 'apps_xs',
onlyIcon: false,
},
{
title: 'Apps scores',
value: MarketplaceDisplayType.SCORES,
icon: 'apps_list',
onlyIcon: false,
contentAfter: (
<IconSvg
name={ isMobile ? 'beta_xs' : 'beta' }
h={ 3 }
w={ isMobile ? 3 : 7 }
ml={ 1 }
/>
),
},
] }
autoWidth
/>
</Skeleton>
) }
<FilterInput
initialValue={ filterQuery }
onChange={ onSearchInputChange }
placeholder="Find app by name or keyword..."
isLoading={ isPlaceholderData }
size={ (feature.securityReportsUrl && isExperiment) ? 'xs' : 'sm' }
flex="1"
/>
</Flex>
<MarketplaceList { (selectedDisplayType === MarketplaceDisplayType.SCORES && feature.securityReportsUrl && isExperiment) ? (
apps={ displayedApps } <MarketplaceListWithScores
onAppClick={ showAppInfo } apps={ displayedApps }
favoriteApps={ favoriteApps } showAppInfo={ showAppInfo }
onFavoriteClick={ onFavoriteClick } favoriteApps={ favoriteApps }
isLoading={ isPlaceholderData } onFavoriteClick={ onFavoriteClick }
showDisclaimer={ showDisclaimer } isLoading={ isPlaceholderData }
selectedCategoryId={ selectedCategoryId } selectedCategoryId={ selectedCategoryId }
/> onAppClick={ handleAppClick }
showContractList={ showContractList }
/>
) : (
<MarketplaceList
apps={ displayedApps }
showAppInfo={ showAppInfo }
favoriteApps={ favoriteApps }
onFavoriteClick={ onFavoriteClick }
isLoading={ isPlaceholderData }
selectedCategoryId={ selectedCategoryId }
onAppClick={ handleAppClick }
/>
) }
{ (selectedApp && isAppInfoModalOpen) && ( { (selectedApp && isAppInfoModalOpen) && (
<MarketplaceAppModal <MarketplaceAppModal
...@@ -178,6 +252,7 @@ const Marketplace = () => { ...@@ -178,6 +252,7 @@ const Marketplace = () => {
isFavorite={ favoriteApps.includes(selectedApp.id) } isFavorite={ favoriteApps.includes(selectedApp.id) }
onFavoriteClick={ onFavoriteClick } onFavoriteClick={ onFavoriteClick }
data={ selectedApp } data={ selectedApp }
showContractList={ showContractList }
/> />
) } ) }
...@@ -188,6 +263,15 @@ const Marketplace = () => { ...@@ -188,6 +263,15 @@ const Marketplace = () => {
appId={ selectedApp.id } appId={ selectedApp.id }
/> />
) } ) }
{ (selectedApp && contractListModalType) && (
<ContractListModal
type={ contractListModalType }
contracts={ selectedApp?.securityReport?.contractsData }
onClose={ clearSelectedAppId }
onBack={ hasPreviousStep ? handleGoBackInContractListModal : undefined }
/>
) }
</> </>
); );
}; };
......
...@@ -20,6 +20,7 @@ import ContentLoader from 'ui/shared/ContentLoader'; ...@@ -20,6 +20,7 @@ import ContentLoader from 'ui/shared/ContentLoader';
import MarketplaceAppTopBar from '../marketplace/MarketplaceAppTopBar'; import MarketplaceAppTopBar from '../marketplace/MarketplaceAppTopBar';
import useAutoConnectWallet from '../marketplace/useAutoConnectWallet'; import useAutoConnectWallet from '../marketplace/useAutoConnectWallet';
import useMarketplaceWallet from '../marketplace/useMarketplaceWallet'; import useMarketplaceWallet from '../marketplace/useMarketplaceWallet';
import useSecurityReports from '../marketplace/useSecurityReports';
const feature = config.features.marketplace; const feature = config.features.marketplace;
...@@ -104,6 +105,8 @@ const MarketplaceApp = () => { ...@@ -104,6 +105,8 @@ const MarketplaceApp = () => {
const { address, sendTransaction, signMessage, signTypedData } = useMarketplaceWallet(id); const { address, sendTransaction, signMessage, signTypedData } = useMarketplaceWallet(id);
useAutoConnectWallet(); useAutoConnectWallet();
const { data: securityReports, isLoading: isSecurityReportsLoading } = useSecurityReports();
const query = useQuery<unknown, ResourceError<unknown>, MarketplaceAppOverview>({ const query = useQuery<unknown, ResourceError<unknown>, MarketplaceAppOverview>({
queryKey: [ 'marketplace-dapps', id ], queryKey: [ 'marketplace-dapps', id ],
queryFn: async() => { queryFn: async() => {
...@@ -140,7 +143,12 @@ const MarketplaceApp = () => { ...@@ -140,7 +143,12 @@ const MarketplaceApp = () => {
return ( return (
<> <>
<MarketplaceAppTopBar data={ data } isLoading={ isPending } isWalletConnected={ Boolean(address) }/> <MarketplaceAppTopBar
data={ data }
isLoading={ isPending || isSecurityReportsLoading }
isWalletConnected={ Boolean(address) }
securityReport={ securityReports?.[id] }
/>
<DappscoutIframeProvider <DappscoutIframeProvider
address={ address } address={ address }
appUrl={ data?.url } appUrl={ data?.url }
......
...@@ -9,6 +9,7 @@ type RadioItemProps = { ...@@ -9,6 +9,7 @@ type RadioItemProps = {
title: string; title: string;
icon?: IconName; icon?: IconName;
onlyIcon: false | undefined; onlyIcon: false | undefined;
contentAfter?: React.ReactNode;
} | { } | {
title: string; title: string;
icon: IconName; icon: IconName;
...@@ -67,9 +68,11 @@ const RadioButton = (props: RadioButtonProps) => { ...@@ -67,9 +68,11 @@ const RadioButton = (props: RadioButtonProps) => {
> >
<input { ...input }/> <input { ...input }/>
<Flex <Flex
alignItems="center"
{ ...checkbox } { ...checkbox }
> >
{ props.title } { props.title }
{ props.contentAfter }
</Flex> </Flex>
</Button> </Button>
); );
...@@ -80,15 +83,22 @@ type RadioButtonGroupProps<T extends string> = { ...@@ -80,15 +83,22 @@ type RadioButtonGroupProps<T extends string> = {
name: string; name: string;
defaultValue: string; defaultValue: string;
options: Array<{ value: T } & RadioItemProps>; options: Array<{ value: T } & RadioItemProps>;
autoWidth?: boolean;
} }
const RadioButtonGroup = <T extends string>({ onChange, name, defaultValue, options }: RadioButtonGroupProps<T>) => { const RadioButtonGroup = <T extends string>({ onChange, name, defaultValue, options, autoWidth = false }: RadioButtonGroupProps<T>) => {
const { getRootProps, getRadioProps } = useRadioGroup({ name, defaultValue, onChange }); const { getRootProps, getRadioProps } = useRadioGroup({ name, defaultValue, onChange });
const group = getRootProps(); const group = getRootProps();
return ( return (
<ButtonGroup { ...group } isAttached size="sm" display="grid" gridTemplateColumns={ `repeat(${ options.length }, 1fr)` }> <ButtonGroup
{ ...group }
isAttached
size="sm"
display="grid"
gridTemplateColumns={ `repeat(${ options.length }, ${ autoWidth ? 'auto' : '1fr' })` }
>
{ options.map((option) => { { options.map((option) => {
const props = getRadioProps({ value: option.value }); const props = getRadioProps({ value: option.value });
return <RadioButton { ...props } key={ option.value } { ...option }/>; return <RadioButton { ...props } key={ option.value } { ...option }/>;
......
import { Button, Skeleton } from '@chakra-ui/react';
import React from 'react';
import IconSvg from 'ui/shared/IconSvg';
import useScoreLevelAndColor from './useScoreLevelAndColor';
interface Props {
className?: string;
score: number;
isLoading?: boolean;
height?: string;
onlyIcon?: boolean;
onClick?: () => void;
}
const SolidityscanReportButton = (
{ className, score, isLoading, height = '32px', onlyIcon, onClick }: Props,
ref: React.ForwardedRef<HTMLButtonElement>,
) => {
const { scoreColor } = useScoreLevelAndColor(score);
return (
<Skeleton isLoaded={ !isLoading } borderRadius="base">
<Button
ref={ ref }
className={ className }
color={ scoreColor }
size="sm"
variant="outline"
colorScheme="gray"
onClick={ onClick }
aria-label="SolidityScan score"
fontWeight={ 500 }
px="6px"
h={ height }
flexShrink={ 0 }
>
<IconSvg name={ score < 80 ? 'score/score-not-ok' : 'score/score-ok' } boxSize={ 5 } mr={ onlyIcon ? 0 : 1 }/>
{ onlyIcon ? null : score }
</Button>
</Skeleton>
);
};
export default React.forwardRef(SolidityscanReportButton);
import { Box, Flex, Text, Grid, useColorModeValue, chakra } from '@chakra-ui/react';
import React from 'react';
import type { SolidityscanReport } from 'types/api/contract';
type DistributionItem = {
id: keyof SolidityscanReport['scan_report']['scan_summary']['issue_severity_distribution'];
name: string;
color: string;
}
const DISTRIBUTION_ITEMS: Array<DistributionItem> = [
{ id: 'critical', name: 'Critical', color: '#891F11' },
{ id: 'high', name: 'High', color: '#EC672C' },
{ id: 'medium', name: 'Medium', color: '#FBE74D' },
{ id: 'low', name: 'Low', color: '#68C88E' },
{ id: 'informational', name: 'Informational', color: '#A3AEBE' },
{ id: 'gas', name: 'Gas', color: '#A47585' },
];
interface Props {
vulnerabilities: SolidityscanReport['scan_report']['scan_summary']['issue_severity_distribution'];
vulnerabilitiesCount: number;
}
type ItemProps = {
item: DistributionItem;
vulnerabilities: SolidityscanReport['scan_report']['scan_summary']['issue_severity_distribution'];
vulnerabilitiesCount: number;
}
const SolidityScanReportItem = ({ item, vulnerabilities, vulnerabilitiesCount }: ItemProps) => {
const bgBar = useColorModeValue('blackAlpha.50', 'whiteAlpha.50');
const yetAnotherGrayColor = useColorModeValue('gray.400', 'gray.500');
return (
<>
<Box w={ 3 } h={ 3 } bg={ item.color } borderRadius="6px" mr={ 2 }></Box>
<Flex justifyContent="space-between" mr={ 3 }>
<Text>{ item.name }</Text>
<Text color={ vulnerabilities[item.id] > 0 ? 'text' : yetAnotherGrayColor }>{ vulnerabilities[item.id] }</Text>
</Flex>
<Box bg={ bgBar } h="10px" borderRadius="8px">
<Box bg={ item.color } w={ vulnerabilities[item.id] / vulnerabilitiesCount } h="10px" borderRadius="8px"/>
</Box>
</>
);
};
const SolidityscanReportDetails = ({ vulnerabilities, vulnerabilitiesCount }: Props) => {
return (
<Grid templateColumns="20px 1fr 100px" alignItems="center" rowGap={ 2 }>
{ DISTRIBUTION_ITEMS.map(item => (
<SolidityScanReportItem item={ item } key={ item.id } vulnerabilities={ vulnerabilities } vulnerabilitiesCount={ vulnerabilitiesCount }/>
)) }
</Grid>
);
};
export default chakra(SolidityscanReportDetails);
import { Box, Flex, Text, chakra, Center, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
import IconSvg from 'ui/shared/IconSvg';
import useScoreLevelAndColor from './useScoreLevelAndColor';
interface Props {
className?: string;
score: number;
}
const SolidityscanReportScore = ({ className, score }: Props) => {
const { scoreLevel, scoreColor } = useScoreLevelAndColor(score);
const chartGrayColor = useColorModeValue('gray.100', 'gray.700');
const yetAnotherGrayColor = useColorModeValue('gray.400', 'gray.500');
const popoverBgColor = useColorModeValue('white', 'gray.900');
return (
<Flex className={ className } alignItems="center">
<Box
w={ 12 }
h={ 12 }
bgGradient={ `conic-gradient(${ scoreColor } 0, ${ scoreColor } ${ score }%, ${ chartGrayColor } 0, ${ chartGrayColor } 100%)` }
borderRadius="24px"
position="relative"
mr={ 3 }
>
<Center position="absolute" w="38px" h="38px" top="5px" right="5px" bg={ popoverBgColor } borderRadius="20px">
<IconSvg name={ score < 80 ? 'score/score-not-ok' : 'score/score-ok' } boxSize={ 5 } color={ scoreColor }/>
</Center>
</Box>
<Box>
<Flex>
<Text color={ scoreColor } fontSize="lg" fontWeight={ 500 }>{ score }</Text>
<Text color={ yetAnotherGrayColor } fontSize="lg" fontWeight={ 500 } whiteSpace="pre"> / 100</Text>
</Flex>
<Text color={ scoreColor } fontWeight={ 500 }>Security score is { scoreLevel }</Text>
</Box>
</Flex>
);
};
export default chakra(SolidityscanReportScore);
import { useColorModeValue } from '@chakra-ui/react';
export default function useScoreLevelAndColor(score: number) {
const greatScoreColor = useColorModeValue('green.600', 'green.400');
const averageScoreColor = useColorModeValue('purple.600', 'purple.400');
const lowScoreColor = useColorModeValue('red.600', 'red.400');
let scoreColor;
let scoreLevel;
if (score >= 80) {
scoreColor = greatScoreColor;
scoreLevel = 'GREAT';
} else if (score >= 30) {
scoreColor = averageScoreColor;
scoreLevel = 'AVERAGE';
} else {
scoreColor = lowScoreColor;
scoreLevel = 'LOW';
}
return { scoreColor, scoreLevel };
}
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