diff --git a/packages/shared/package.json b/packages/shared/package.json index e628467f5..fc507ec89 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -1,6 +1,6 @@ { "name": "@hats-finance/shared", - "version": "1.1.22", + "version": "1.1.28", "description": "", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/shared/src/constants/index.ts b/packages/shared/src/constants/index.ts new file mode 100644 index 000000000..44fed736e --- /dev/null +++ b/packages/shared/src/constants/index.ts @@ -0,0 +1 @@ +export * from "./mdAllowedElements"; diff --git a/packages/shared/src/constants/mdAllowedElements.ts b/packages/shared/src/constants/mdAllowedElements.ts new file mode 100644 index 000000000..ee277a4c8 --- /dev/null +++ b/packages/shared/src/constants/mdAllowedElements.ts @@ -0,0 +1,49 @@ +export const allowedElementsMarkdown = [ + "a", + "article", + "b", + "blockquote", + "br", + "caption", + "code", + "del", + "details", + "div", + "em", + "h1", + "h2", + "h3", + "h4", + "h5", + "h6", + "hr", + "i", + "img", + "ins", + "kbd", + "li", + "main", + "ol", + "p", + "pre", + "section", + "span", + "strike", + "strong", + "sub", + "summary", + "sup", + "table", + "tbody", + "td", + "th", + "thead", + "tr", + "u", + "ul", +]; + +export const allowedAttributesMarkdown = { + a: ["href", "name", "class"], + img: ["src", "alt", "class"], +}; diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index d914b28ea..191ca7765 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -3,3 +3,4 @@ export * from "./severities"; export * from "./abis"; export * from "./config"; export * from "./utils"; +export * from "./constants"; diff --git a/packages/shared/src/types/editor.ts b/packages/shared/src/types/editor.ts index d710ef4e4..bc141e9a2 100644 --- a/packages/shared/src/types/editor.ts +++ b/packages/shared/src/types/editor.ts @@ -73,6 +73,8 @@ export interface IBaseEditedVaultDescription { name: string; tokenIcon: string; type?: IVaultType; + isPrivateAudit?: boolean; + whitelist: { address: string }[]; endtime?: number; starttime?: number; emails: IEditedCommunicationEmail[]; diff --git a/packages/shared/src/types/index.ts b/packages/shared/src/types/index.ts index fe44bbdf2..eb1fc4f0f 100644 --- a/packages/shared/src/types/index.ts +++ b/packages/shared/src/types/index.ts @@ -2,3 +2,4 @@ export * from "./types"; export * from "./editor"; export * from "./payout"; export * from "./safe"; +export * from "./submissions"; diff --git a/packages/shared/src/types/submissions.ts b/packages/shared/src/types/submissions.ts new file mode 100644 index 000000000..6eac37e18 --- /dev/null +++ b/packages/shared/src/types/submissions.ts @@ -0,0 +1,6 @@ +export type ISubmissionMessageObject = { + ref?: string; + isEncryptedByHats?: boolean; + decrypted: string | undefined; + encrypted: string; +}; diff --git a/packages/shared/src/types/types.ts b/packages/shared/src/types/types.ts index 2566ce682..51f07ae92 100644 --- a/packages/shared/src/types/types.ts +++ b/packages/shared/src/types/types.ts @@ -1,3 +1,5 @@ +import { ISubmissionMessageObject } from "./submissions"; + export interface IVaultInfo { version: IVault["version"]; address: string; @@ -120,6 +122,8 @@ interface IBaseVaultDescription { name: string; tokenIcon: string; type?: IVaultType; + isPrivateAudit?: boolean; + whitelist: { address: string }[]; endtime?: number; starttime?: number; oneLiner?: string; @@ -274,7 +278,7 @@ export interface ISubmittedSubmission { subId: string; submissionIdx: number; submissionHash: string; - submissionData?: string; + submissionData?: ISubmissionMessageObject; submissionDecrypted?: string; linkedVault?: IVault; submissionDataStructure?: { diff --git a/packages/shared/src/utils/vaults.utils.ts b/packages/shared/src/utils/vaults.utils.ts index def980f80..cc9164984 100644 --- a/packages/shared/src/utils/vaults.utils.ts +++ b/packages/shared/src/utils/vaults.utils.ts @@ -5,7 +5,7 @@ import { isAddressAMultisigMember } from "./gnosis.utils"; import { isValidIpfsHash } from "./ipfs.utils"; import { fixObject } from "./vaultEditor.utils"; -export type IVaultInfoWithCommittee = IVaultInfo & { committee: string }; +export type IVaultInfoWithCommittee = IVaultInfo & { committee: string; registered: boolean }; export const getAllVaultsInfoWithCommittee = async (): Promise => { try { @@ -14,6 +14,7 @@ export const getAllVaultsInfoWithCommittee = async (): Promise { }); }); - const subgraphResults = (await Promise.all(subgraphPromises) - .then((responses) => responses.map((res) => res.data)) - .catch((error) => { - console.error("Error fetching subgraph data:", error); - })) + const subgraphResults = ( + await Promise.all(subgraphPromises) + .then((responses) => responses.map((res) => res.data)) + .catch((error) => { + console.error("Error fetching subgraph data:", error); + }) + ) .filter((res) => res != null) .map((res) => res.data.vaults) .flat() @@ -71,7 +73,8 @@ const buildSitemap = async () => { const descriptionsData = await Promise.all(descriptionsResults.map((res) => res.data)); const descriptionsDataWithHash = descriptionsData.map((d) => ({ ...d, hash: descriptionHashes[descriptionsData.indexOf(d)] })); const descriptions = descriptionsDataWithHash.filter((d) => !(d instanceof Error)); - const vaultsRoutes = descriptions.map((d) => { + const descriptionsNoPrivateAudits = descriptions.filter((d) => !d["project-metadata"]?.isPrivateAudit); + const vaultsRoutes = descriptionsNoPrivateAudits.map((d) => { const vaultId = subgraphResults.find((vault) => vault.descriptionHash === d.hash).id; const vaultSlug = slugify(d["project-metadata"]?.name ?? d["Project-metadata"]?.name ?? ""); const isAudit = @@ -93,15 +96,15 @@ const buildSitemap = async () => { const vaultsRoutesXml = vaultsRoutes.reduce( (acc, route) => `${acc} - ${appendPathAndGenerateUrl(route, 'rewards')} - ${appendPathAndGenerateUrl(route, 'deposits')} - ${appendPathAndGenerateUrl(route, 'scope')}`, + ${appendPathAndGenerateUrl(route, "rewards")} + ${appendPathAndGenerateUrl(route, "deposits")} + ${appendPathAndGenerateUrl(route, "scope")}`, "" ); const routesXml = routes.reduce( (acc, route) => `${acc} - ${appendPathAndGenerateUrl(route, '')}`, + ${appendPathAndGenerateUrl(route, "")}`, "" ); diff --git a/packages/web/src/components/FormControls/FormInput/FormInput.tsx b/packages/web/src/components/FormControls/FormInput/FormInput.tsx index b9a1c356f..262612e61 100644 --- a/packages/web/src/components/FormControls/FormInput/FormInput.tsx +++ b/packages/web/src/components/FormControls/FormInput/FormInput.tsx @@ -20,6 +20,7 @@ type FormInputProps = { copyable?: boolean; disabled?: boolean; removable?: boolean; + flexExpand?: boolean; colorable?: boolean; isDirty?: boolean; noMargin?: boolean; @@ -35,6 +36,7 @@ function FormInputComponent( pastable = false, copyable = false, removable = false, + flexExpand = false, noErrorLabel = false, type = "text", colorable = false, @@ -220,6 +222,7 @@ function FormInputComponent( colorable={colorable} readOnly={readOnly} className={className} + flexExpand={flexExpand} >
{label !== undefined && ( diff --git a/packages/web/src/components/FormControls/FormInput/styles.ts b/packages/web/src/components/FormControls/FormInput/styles.ts index 501200ef2..314f094b5 100644 --- a/packages/web/src/components/FormControls/FormInput/styles.ts +++ b/packages/web/src/components/FormControls/FormInput/styles.ts @@ -14,6 +14,7 @@ type StyledFormInputProps = { colorable?: boolean; readOnly?: boolean; disabled?: boolean; + flexExpand?: boolean; type?: FormInputType; }; @@ -31,11 +32,13 @@ export const StyledFormInput = styled.div( disabled, colorable, readOnly, + flexExpand, }) => css` position: relative; overflow: hidden; margin-bottom: ${noMargin ? 0 : getSpacing(3)}; - width: 100%; + width: ${flexExpand ? "unset" : "100%"}; + flex: ${flexExpand ? 1 : "unset"}; .main-container { position: relative; diff --git a/packages/web/src/components/FormControls/FormMDEditor/FormMDEditor.tsx b/packages/web/src/components/FormControls/FormMDEditor/FormMDEditor.tsx index 9ca195faf..b61904d32 100644 --- a/packages/web/src/components/FormControls/FormMDEditor/FormMDEditor.tsx +++ b/packages/web/src/components/FormControls/FormMDEditor/FormMDEditor.tsx @@ -1,5 +1,5 @@ +import { allowedElementsMarkdown } from "@hats-finance/shared"; import MDEditor, { PreviewType } from "@uiw/react-md-editor"; -import { disallowedElementsMarkdown } from "constants/constants"; import { forwardRef } from "react"; import { parseIsDirty } from "../utils"; import { StyledFormMDEditor } from "./styles"; @@ -37,7 +37,7 @@ export const FormMDEditorComponent = ( > (disabled ? undefined : onChange(value ?? ""))} minHeight={200} height={400} diff --git a/packages/web/src/components/FormControls/FormSelectInput/FormSelectInput.tsx b/packages/web/src/components/FormControls/FormSelectInput/FormSelectInput.tsx index dc8fbf68b..bff22306b 100644 --- a/packages/web/src/components/FormControls/FormSelectInput/FormSelectInput.tsx +++ b/packages/web/src/components/FormControls/FormSelectInput/FormSelectInput.tsx @@ -25,6 +25,7 @@ interface FormSelectInputProps { disabled?: boolean; noMargin?: boolean; readOnly?: boolean; + flexExpand?: boolean; isDirty?: boolean | boolean[]; value: string | string[]; onChange?: (data: string | string[]) => void; @@ -43,6 +44,7 @@ export function FormSelectInputComponent( disabled = false, isDirty = false, readOnly = false, + flexExpand = false, noMargin = false, emptyState, error, @@ -85,7 +87,7 @@ export function FormSelectInputComponent( }; return ( - + {label && } ( - ({ noMargin }) => css` +export const StyledFormSelectInput = styled.div<{ noMargin: boolean; flexExpand: boolean }>( + ({ noMargin, flexExpand }) => css` position: relative; margin-bottom: ${noMargin ? 0 : getSpacing(3)}; - width: 100%; + width: ${flexExpand ? "unset" : "100%"}; + flex: ${flexExpand ? 1 : "unset"}; label.input-label { display: block; diff --git a/packages/web/src/components/Sidebar/NavLinks/NavLinks.tsx b/packages/web/src/components/Sidebar/NavLinks/NavLinks.tsx index 70f799be4..1587bdb50 100644 --- a/packages/web/src/components/Sidebar/NavLinks/NavLinks.tsx +++ b/packages/web/src/components/Sidebar/NavLinks/NavLinks.tsx @@ -27,6 +27,7 @@ export default function NavLinks() { const { chain } = useNetwork(); const { address } = useAccount(); + const [isInvitedToPrivateAudits, setIsInvitedToPrivateAudits] = useState(false); const [isCommitteeAddress, setIsCommitteeAddress] = useState(false); const [showCommitteeToolsSubroutes, setshowCommitteeToolsSubroutes] = useState(false); const committeeToolsSubrouteRef = useRef(null); @@ -37,6 +38,21 @@ export default function NavLinks() { setshowCommitteeToolsSubroutes(false); }; + useEffect(() => { + if (!allVaultsOnEnv || !address) return setIsInvitedToPrivateAudits(false); + + const isInvited = + allVaultsOnEnv.some( + (vault) => + vault.description?.["project-metadata"].isPrivateAudit && + vault.description?.["project-metadata"].whitelist.some( + (whiteAddress) => whiteAddress.address.toLowerCase() === address.toLowerCase() + ) + ) ?? false; + + setIsInvitedToPrivateAudits(isInvited); + }, [allVaultsOnEnv, address]); + useEffect(() => { const verifyIfCommitteeAddress = async () => { try { @@ -69,6 +85,16 @@ export default function NavLinks() {

{t("auditCompetitions")}

{t("competitions")}

+ {/*

{t("submitVulnerability")}

diff --git a/packages/web/src/components/VaultCard/VaultCard.tsx b/packages/web/src/components/VaultCard/VaultCard.tsx index c35c4f158..83d0386ad 100644 --- a/packages/web/src/components/VaultCard/VaultCard.tsx +++ b/packages/web/src/components/VaultCard/VaultCard.tsx @@ -267,7 +267,7 @@ export const VaultCard = ({ ? millify(totalPaidOutOnAudit?.usd ?? 0) : showIntended ? millify(vault.amountsInfo?.competitionIntendedAmount?.deposited.usd ?? 0) - : millify(vault.amountsInfo?.depositedAmount.usd ?? 0)} + : millify(vault.amountsInfo?.maxRewardAmount.usd ?? 0)}
diff --git a/packages/web/src/components/VaultSeverityRewardCard/VaultSeverityRewardCard.tsx b/packages/web/src/components/VaultSeverityRewardCard/VaultSeverityRewardCard.tsx index a285eac5b..f48cd5363 100644 --- a/packages/web/src/components/VaultSeverityRewardCard/VaultSeverityRewardCard.tsx +++ b/packages/web/src/components/VaultSeverityRewardCard/VaultSeverityRewardCard.tsx @@ -1,5 +1,6 @@ -import { IVault, IVulnerabilitySeverity } from "@hats-finance/shared"; -import { Pill, VaultNftRewardCard } from "components"; +import { IVault, IVulnerabilitySeverity, IVulnerabilitySeverityV2 } from "@hats-finance/shared"; +import InfoIcon from "@mui/icons-material/InfoOutlined"; +import { Pill, VaultNftRewardCard, WithTooltip } from "components"; import { useSeverityRewardInfo } from "hooks/severities/useSeverityRewardInfo"; import { useTranslation } from "react-i18next"; import { formatNumber } from "utils"; @@ -14,20 +15,40 @@ interface VaultSeverityRewardCardProps { export function VaultSeverityRewardCard({ vault, severity, severityIndex, noNft = false }: VaultSeverityRewardCardProps) { const { t } = useTranslation(); - const { rewardPercentage, rewardPrice, rewardColor } = useSeverityRewardInfo(vault, severityIndex); + const { rewardPercentage, rewardPrice, rewardCap, rewardColor } = useSeverityRewardInfo(vault, severityIndex); const severityName = severity?.name.toLowerCase().replace("severity", "") ?? ""; + const showCap = vault.version === "v2" && vault.description?.severities.some((sev) => !!sev.capAmount); return ( - - + +
+ +
{`${rewardPercentage.toFixed(2)}%`} -  {t("ofVault")}  +  {t("ofRewards")} 
~{`$${formatNumber(rewardPrice)}`}
+ {showCap && ( + <> + {(severity as IVulnerabilitySeverityV2).capAmount ? ( + +
+
+ {t("maxRewardCap")} + +
+ ~{`$${formatNumber(rewardCap)}`} +
+
+ ) : ( +
+ )} + + )} {!noNft && (
diff --git a/packages/web/src/components/VaultSeverityRewardCard/styles.ts b/packages/web/src/components/VaultSeverityRewardCard/styles.ts index 72c3f998b..961a7ebe1 100644 --- a/packages/web/src/components/VaultSeverityRewardCard/styles.ts +++ b/packages/web/src/components/VaultSeverityRewardCard/styles.ts @@ -2,13 +2,13 @@ import styled, { css } from "styled-components"; import { getSpacing } from "styles"; import { breakpointsDefinition } from "styles/breakpoints.styles"; -export const StyledVaultSeverityRewardCard = styled.div<{ color: string; noNft: boolean }>( - ({ color, noNft }) => css` +export const StyledVaultSeverityRewardCard = styled.div<{ color: string; columns: number }>( + ({ color, columns }) => css` display: grid; - grid-template-columns: ${noNft ? "3fr 4fr" : "1fr 1fr 1fr"}; + grid-template-columns: ${columns === 2 ? "3fr 4fr" : columns === 3 ? "1fr 1fr 1fr" : "1fr 1fr 1fr 1fr"}; align-items: center; justify-content: space-between; - gap: ${getSpacing(1)}; + gap: ${getSpacing(2)}; .severity-name, .severity-prize, @@ -24,11 +24,16 @@ export const StyledVaultSeverityRewardCard = styled.div<{ color: string; noNft: } @media (max-width: ${breakpointsDefinition.mediumMobile}) { - grid-template-columns: ${noNft ? "1fr 1fr" : "2fr 1fr 1fr"}; + grid-template-columns: ${columns === 2 ? "1fr 1fr" : columns === 3 ? "1fr 1fr 1fr" : "1fr 1fr 1fr 1fr"}; } @media (max-width: ${breakpointsDefinition.smallMobile}) { - grid-template-columns: ${noNft ? "1fr 1fr" : "4fr 4fr 3fr"}; + grid-template-columns: ${columns === 2 ? "1fr" : columns === 3 ? "1fr 1fr" : "1fr 1fr"}; + + .severity-name { + grid-column-start: 1; + grid-column-end: ${columns}; + } } .severity-name { @@ -39,10 +44,17 @@ export const StyledVaultSeverityRewardCard = styled.div<{ color: string; noNft: .severity-prize { font-weight: 700; + flex-direction: column; + align-items: flex-end; + + @media (max-width: ${breakpointsDefinition.smallMobile}) { + align-items: center; + } - @media (max-width: ${breakpointsDefinition.mediumMobile}) { - flex-direction: column; - align-items: flex-end; + .title-container { + display: flex; + align-items: center; + gap: ${getSpacing(0.2)}; } .tiny { @@ -51,6 +63,7 @@ export const StyledVaultSeverityRewardCard = styled.div<{ color: string; noNft: } .price { + font-size: var(--small); color: ${color}; font-weight: 700; } diff --git a/packages/web/src/constants/constants.ts b/packages/web/src/constants/constants.ts index 43363dcf6..ae1af7f79 100644 --- a/packages/web/src/constants/constants.ts +++ b/packages/web/src/constants/constants.ts @@ -121,5 +121,3 @@ export enum Transactions { export const HAT_TOKEN_ADDRESS_V1 = "0x436cA314A2e6FfDE52ba789b257b51DaCE778F1a"; export const HAT_TOKEN_DECIMALS_V1 = "18"; export const HAT_TOKEN_SYMBOL_V1 = "HAT"; - -export const disallowedElementsMarkdown = ["script", "iframe", "img", "audio", "video", "object", "embed"]; diff --git a/packages/web/src/graphql/balancer.ts b/packages/web/src/graphql/balancer.ts new file mode 100644 index 000000000..21d48a922 --- /dev/null +++ b/packages/web/src/graphql/balancer.ts @@ -0,0 +1,12 @@ +export const GET_PRICES_BALANCER = ` + query getPrices { + tokenGetCurrentPrices { + price + address + } + } +`; + +export type IBalancerGetPricesResponse = { + data: { tokenGetCurrentPrices: { address: string; price: string }[] }; +}; diff --git a/packages/web/src/hooks/severities/useSeverityRewardInfo.ts b/packages/web/src/hooks/severities/useSeverityRewardInfo.ts index 3088de7f6..82326de47 100644 --- a/packages/web/src/hooks/severities/useSeverityRewardInfo.ts +++ b/packages/web/src/hooks/severities/useSeverityRewardInfo.ts @@ -1,4 +1,3 @@ -import { formatUnits } from "ethers/lib/utils"; import { useVaultsTotalPrices } from "hooks/vaults/useVaultsTotalPrices"; import { IVault } from "types"; import { generateColorsArrayInBetween } from "utils/colors.utils"; @@ -15,53 +14,53 @@ export const getSeveritiesColorsArray = (vault: IVault | undefined): string[] => export function useSeverityRewardInfo(vault: IVault | undefined, severityIndex: number) { const { totalPrices } = useVaultsTotalPrices(vault ? vault.multipleVaults ?? [vault] : []); - if (!vault || !vault.description) return { rewardPrice: 0, rewardPercentage: 0, rewardColor: INITIAL_SEVERITY_COLOR }; + if (!vault || !vault.description) + return { rewardPrice: 0, rewardPercentage: 0, rewardCap: 0, rewardColor: INITIAL_SEVERITY_COLOR }; - const isAudit = vault.description && vault.description["project-metadata"].type === "audit"; const showIntendedAmounts = vault.amountsInfo?.showCompetitionIntendedAmount ?? false; const SEVERITIES_COLORS = getSeveritiesColorsArray(vault); if (vault.version === "v2") { const severity = vault.description.severities[severityIndex]; - if (!severity) return { rewardPrice: 0, rewardPercentage: 0, rewardColor: INITIAL_SEVERITY_COLOR }; + if (!severity) return { rewardPrice: 0, rewardPercentage: 0, rewardCap: 0, rewardColor: INITIAL_SEVERITY_COLOR }; const sumTotalPrices = Object.values(totalPrices).reduce((a, b = 0) => a + b, 0); - // const maxBountyPercentage = Number(vault.maxBounty) / 10000; // Number between 0 and 1; - // TODO: remove this when we have the new vault contract version - const maxBountyPercentage = Number(isAudit ? 10000 : vault.maxBounty) / 10000; - const rewardPercentage = +severity.percentage * maxBountyPercentage; + const rewardPercentage = +severity.percentage; let rewardPrice: number = 0; + let rewardCap: number = 0; if (vault.multipleVaults && sumTotalPrices) { rewardPrice = sumTotalPrices * (rewardPercentage / 100); } else if (vault.amountsInfo?.tokenPriceUsd) { rewardPrice = (showIntendedAmounts ? vault.amountsInfo.competitionIntendedAmount?.deposited.tokens ?? 0 - : Number(formatUnits(vault.honeyPotBalance, vault.stakingTokenDecimals))) * + : vault.amountsInfo.maxRewardAmount.tokens) * (rewardPercentage / 100) * vault.amountsInfo?.tokenPriceUsd; + rewardCap = (severity.capAmount ?? 0) * vault.amountsInfo?.tokenPriceUsd; } const orderedSeverities = vault.description.severities.map((severity) => severity.percentage).sort((a, b) => a - b); const rewardColor: string = SEVERITIES_COLORS[orderedSeverities.indexOf(severity.percentage) ?? 0]; - return { rewardPrice, rewardPercentage, rewardColor }; + return { rewardPrice, rewardPercentage, rewardCap, rewardColor }; } else { const severity = vault.description.severities[severityIndex]; - if (!severity) return { rewardPrice: 0, rewardPercentage: 0, rewardColor: INITIAL_SEVERITY_COLOR }; + if (!severity) return { rewardPrice: 0, rewardPercentage: 0, rewardCap: 0, rewardColor: INITIAL_SEVERITY_COLOR }; const sumTotalPrices = Object.values(totalPrices).reduce((a, b = 0) => a + b, 0); const rewardPercentage = (Number(vault.rewardsLevels[severity.index]) / 10000) * 100; let rewardPrice: number = 0; + let rewardCap: number = 0; if (vault.multipleVaults && sumTotalPrices) { rewardPrice = sumTotalPrices * (rewardPercentage / 100); } else if (vault.amountsInfo?.tokenPriceUsd) { rewardPrice = (showIntendedAmounts ? vault.amountsInfo.competitionIntendedAmount?.deposited.tokens ?? 0 - : Number(formatUnits(vault.honeyPotBalance, vault.stakingTokenDecimals))) * + : vault.amountsInfo.maxRewardAmount.tokens) * (rewardPercentage / 100) * vault.amountsInfo?.tokenPriceUsd; } @@ -69,6 +68,6 @@ export function useSeverityRewardInfo(vault: IVault | undefined, severityIndex: const orderedSeverities = vault.description.severities.map((severity) => severity.index).sort((a, b) => a - b); const rewardColor: string = SEVERITIES_COLORS[orderedSeverities.indexOf(severity.index) ?? 0]; - return { rewardPrice, rewardPercentage, rewardColor }; + return { rewardPrice, rewardPercentage, rewardCap, rewardColor }; } } diff --git a/packages/web/src/hooks/subgraph/submissions/useSubmissions.tsx b/packages/web/src/hooks/subgraph/submissions/useSubmissions.tsx index a03012034..f1b40b27d 100644 --- a/packages/web/src/hooks/subgraph/submissions/useSubmissions.tsx +++ b/packages/web/src/hooks/subgraph/submissions/useSubmissions.tsx @@ -1,4 +1,4 @@ -import { ISubmittedSubmission } from "@hats-finance/shared"; +import { ISubmissionMessageObject, ISubmittedSubmission } from "@hats-finance/shared"; import axios from "axios"; import { LocalStorage } from "constants/constants"; import { blacklistedWallets } from "data/blacklistedWallets"; @@ -41,7 +41,7 @@ export function SubmissionsProvider({ children }: PropsWithChildren<{}>) { const { multiChainData, allChainsLoaded } = useMultiChainSubmissions(); const setSubmissionsWithDetails = async (submissionsData: ISubmittedSubmission[]) => { - const loadSubmissionData = async (submission: ISubmittedSubmission): Promise => { + const loadSubmissionData = async (submission: ISubmittedSubmission): Promise => { if (isValidIpfsHash(submission.submissionHash)) { try { const dataResponse = await axios.get(ipfsTransformUri(submission.submissionHash)); diff --git a/packages/web/src/hooks/subgraph/vaults/parser.ts b/packages/web/src/hooks/subgraph/vaults/parser.ts index 1b0e1dbcf..2691a9612 100644 --- a/packages/web/src/hooks/subgraph/vaults/parser.ts +++ b/packages/web/src/hooks/subgraph/vaults/parser.ts @@ -1,6 +1,6 @@ import { formatUnits } from "@ethersproject/units"; import { IMaster, IPayoutGraph, IUserNft, IVault } from "@hats-finance/shared"; -import { BigNumber } from "ethers"; +import { BigNumber, ethers } from "ethers"; import { appChains } from "settings"; export const parseMasters = (masters: IMaster[], chainId: number) => { @@ -49,14 +49,30 @@ export const populateVaultsWithPricing = (vaults: IVault[], tokenPrices: number[ const isTestnet = appChains[vault.chainId].chain.testnet; const tokenPrice: number = isTestnet ? 1387.65 : (tokenPrices && tokenPrices[vault.stakingToken]) ?? 0; const depositedAmountTokens = Number(formatUnits(vault.honeyPotBalance, vault.stakingTokenDecimals)); + const isAudit = vault.description?.["project-metadata"].type === "audit"; - const maxRewardFactor = vault.version === "v1" ? +vault.rewardsLevels[vault.rewardsLevels.length - 1] : +vault.maxBounty; + const governanceSplit = BigNumber.from(vault.governanceHatRewardSplit).eq(ethers.constants.MaxUint256) + ? vault.master.defaultGovernanceHatRewardSplit + : vault.governanceHatRewardSplit; + const hackerHatsSplit = BigNumber.from(vault.hackerHatRewardSplit).eq(ethers.constants.MaxUint256) + ? vault.master.defaultHackerHatRewardSplit + : vault.hackerHatRewardSplit; + + // In v2 vaults the split sum (immediate, vested, committee) is 100%. So we need to calculate the split factor to get the correct values. + // In v1 this is not a probem. So the factor is 1. + const splitFactor = vault.version === "v1" ? 1 : (10000 - Number(governanceSplit) - Number(hackerHatsSplit)) / 100 / 100; + + const governanceFee = Number(governanceSplit) / 100 / 100; + const committeeFee = (Number(vault.committeeRewardSplit) / 100 / 100) * splitFactor; + + const maxRewardFactor = 1 - governanceFee - committeeFee; return { ...vault, amountsInfo: { showCompetitionIntendedAmount: - vault.description?.["project-metadata"].type === "audit" && + isAudit && + vault.description && vault.description["project-metadata"].starttime && vault.description["project-metadata"].starttime > new Date().getTime() / 1000 + 48 * 3600 && // 48 hours !!vault.description?.["project-metadata"].intendedCompetitionAmount && @@ -69,9 +85,8 @@ export const populateVaultsWithPricing = (vaults: IVault[], tokenPrices: number[ usd: +vault.description?.["project-metadata"].intendedCompetitionAmount * tokenPrice, }, maxReward: { - tokens: +vault.description?.["project-metadata"].intendedCompetitionAmount * (maxRewardFactor / 100 / 100), - usd: - +vault.description?.["project-metadata"].intendedCompetitionAmount * tokenPrice * (maxRewardFactor / 100 / 100), + tokens: +vault.description?.["project-metadata"].intendedCompetitionAmount * maxRewardFactor, + usd: +vault.description?.["project-metadata"].intendedCompetitionAmount * tokenPrice * maxRewardFactor, }, } : undefined, @@ -80,8 +95,8 @@ export const populateVaultsWithPricing = (vaults: IVault[], tokenPrices: number[ usd: depositedAmountTokens * tokenPrice, }, maxRewardAmount: { - tokens: depositedAmountTokens * (maxRewardFactor / 100 / 100), - usd: depositedAmountTokens * tokenPrice * (maxRewardFactor / 100 / 100), + tokens: depositedAmountTokens * maxRewardFactor, + usd: depositedAmountTokens * tokenPrice * maxRewardFactor, }, }, } as IVault; diff --git a/packages/web/src/hooks/subgraph/vaults/useVaults.tsx b/packages/web/src/hooks/subgraph/vaults/useVaults.tsx index a92497f30..17087bc78 100644 --- a/packages/web/src/hooks/subgraph/vaults/useVaults.tsx +++ b/packages/web/src/hooks/subgraph/vaults/useVaults.tsx @@ -16,7 +16,7 @@ import { PropsWithChildren, createContext, useContext, useEffect, useState } fro import { IS_PROD, appChains } from "settings"; import { ipfsTransformUri } from "utils"; import { isValidIpfsHash } from "utils/ipfs.utils"; -import { getCoingeckoTokensPrices, getUniswapTokenPrices } from "utils/tokens.utils"; +import { getBalancerTokenPrices, getCoingeckoTokensPrices, getUniswapTokenPrices } from "utils/tokens.utils"; import { useAccount, useNetwork } from "wagmi"; import { useLiveSafetyPeriod } from "../../useLiveSafetyPeriod"; import { populateVaultsWithPricing } from "./parser"; @@ -129,6 +129,25 @@ export function VaultsProvider({ children }: PropsWithChildren<{}>) { console.error(error); } + // Get prices from Balancer + try { + const tokensLeft = stakingTokens.filter((token) => !(token.address in foundTokenPrices)); + const balancerTokenPrices = await getBalancerTokenPrices(tokensLeft); + + console.log({ balancerTokenPrices, tokensLeft }); + + if (balancerTokenPrices) { + tokensLeft.forEach((token) => { + if (balancerTokenPrices.hasOwnProperty(token.address)) { + const price = balancerTokenPrices[token.address]?.["usd"]; + if (price && +price > 0) foundTokenPrices[token.address] = +price; + } + }); + } + } catch (error) { + console.error(error); + } + return foundTokenPrices; }; diff --git a/packages/web/src/languages/en.json b/packages/web/src/languages/en.json index 268eef84f..29524e24f 100644 --- a/packages/web/src/languages/en.json +++ b/packages/web/src/languages/en.json @@ -253,7 +253,8 @@ "existingEditSessionsHelperPendingApproval": "These are the editions request you did to your vault. You have currently one edition waiting for approval, please wait until governance approve it or if you want you can cancel the approval request.", "cancelApprovalRequest": "Cancel approval request", "vaultRepoInformationExplanation": "Please put here all the code repositories where the code/contracts are.", - "vaultRepoInformationExplanationAudit": "Please put here all the code repositories where the contracts are, along with the specific commit hash. \n\nThis is used for creating a fork of the repo (the one you selected as main) and creating issues for each security researcher submission. Also, we are going to show this information on the vault details page (as soon as the repo is public)", + "vaultRepoInformationExplanationAudit": "Please put here the code repositoriy where the contracts are, along with the specific commit hash. \n\nThis is used for creating a fork of the repo and creating issues for each security researcher submission. Also, we are going to show this information on the vault details page (as soon as the repo is public)", + "vaultRepoInformationExplanationPrivateAudit": "Please put here the code repositoriy where the contracts are, along with the specific commit hash. \n\nThis is used for creating a fork of the repo and creating issues for each security researcher submission.", "youHaveNotSelectedRepos": "You have not specified any repository information.", "youHaveNotSelectedAnyContracts": "You have not specified any contract information.", "cancel": "Cancel", @@ -425,6 +426,9 @@ "totalRewards": "Total rewards", "totalDeposits": "Total deposits", "deposited": "Deposited", + "livePrivateCompetitions": "Live private competitions", + "upcomingPrivateCompetitions": "Upcoming private competitions", + "finishedPrivateCompetitions": "Finished private competitions", "liveCompetitions": "Live competitions", "upcomingCompetitions": "Upcoming competitions", "finishedCompetitions": "Finished competitions", @@ -582,6 +586,9 @@ "goToProjectWebsite": "Go to project website", "doYouWantToGoToProjectWebsite": "Do you want to go to project website? \n ({{website}})", "yesGo": "Yes, go", + "ofRewards": "of rewards", + "maxRewardCap": "Max reward cap", + "maxRewardCapExplanation": "This is the maximum amount that will be paid for a single submission.", "clearSubmission": "Clear submission", "clearSubmissionExplanation": "Are you sure you want to clear this submission? \n\n This will remove all the information of the submission.", "clearForm": "Clear form", @@ -590,10 +597,23 @@ "projectNotAvailable": "Project not available", "projectNotAvailableExplanation": "The project on which you are attempting to submit a vulnerability is no longer available. \n\n If you think this is an error, please contact Hats support.", "understand": "Understand", + "thereIsNoPublicSubmission": "There are no public submission for this vault yet.", + "gettingSubmissions": "Getting submissions", + "seeSubmissionsOnGithub": "See submissions on GitHub", + "openGithub": "Open GitHub", + "doYouWantToSeeSubmissionsOnGithub": "Do you want to see the submissions on GitHub?", "submissionChanged": "Submission changed", "submissionChangedExplanationAuditWizard": "The submission you are trying to edit has changed. \n\n If you want to edit the submission, please go back to Audit Wizard.", "submissionNotValid": "Submission not valid", "submissionNotValidExplanationAuditWizard": "The submission you are trying to edit is not valid. \n\n Please go back to Audit Wizard and try again.", + "isPrivateQuestion": "Is private?", + "scopePrivateAuditsWarning": "This is a private audit competition.\nFor this vault, all the scope information should be under the private github repository.", + "privateAuditSubmissionsOnlyOnGithub": "This is a private audit competition. \nAll the submissions should be on the private GitHub repository. You can go to the github clicking on the button below.", + "privateAuditCompetitions": "Private audit competitions", + "privateCompetitions": "Private competitions", + "pleaseSignInWithEthereumToSeePrivateComps": "Please sign in with Ethereum (SIWE) to see private competitions.", + "youAreNotInvitedToAnyPrivateComps": "You are not invited to any private competitions.", + "privateAuditSubmissionsScopeInGithub": "This is a private audit competition. \nAll the scope information will be on the private GitHub repository. You can go to the github clicking on the button below.", "PGPTool": { "title": "PGP tool", "unlockPgpTool": "Unlock the PGP tool", @@ -1110,7 +1130,12 @@ "communicationChannelExplanation": "Provide a communication channel for the committee to receive the vulnerability submissions.", "startEndDateExplanation": "Provide the start and end date of the vault.", "oneLiner": "One liner (brief description)", - "oneLiner-placeholder": "Enter a brief description about your project" + "oneLiner-placeholder": "Enter a brief description about your project", + "whitelistedAddreses": "Whitelisted addresses", + "whitelistedAddresesExplanation": "Provide the list of addresses that can see and submit vulnerabilities to the vault.", + "newWhitelistedAddress": "New white-listed address", + "whitelistedAddress": "White-listed address", + "whitelistedAddress-placeholder": "Enter wallet address" }, "signatureMessage": "I hereby confirm the details in ipfs hash {{ipfsHash}}.", "committee-details": "Committee Details", diff --git a/packages/web/src/pages/CommitteeTools/DecryptionTool/DecryptionPage/DecryptionPage.tsx b/packages/web/src/pages/CommitteeTools/DecryptionTool/DecryptionPage/DecryptionPage.tsx index d2886f200..d2ff63b96 100644 --- a/packages/web/src/pages/CommitteeTools/DecryptionTool/DecryptionPage/DecryptionPage.tsx +++ b/packages/web/src/pages/CommitteeTools/DecryptionTool/DecryptionPage/DecryptionPage.tsx @@ -1,3 +1,4 @@ +import { ISubmissionMessageObject } from "@hats-finance/shared"; import { yupResolver } from "@hookform/resolvers/yup"; import KeyIcon from "@mui/icons-material/KeyOutlined"; import EyeIcon from "@mui/icons-material/VisibilityOutlined"; @@ -53,11 +54,14 @@ export const DecryptionPage = () => { let decryptedPart = ""; let encryptedPart = ""; + // let isDecryptedPartEncryptedByHats = false; + // TODO: private audits V2 (We need to check if the message is encrypted by HATS, if so, decrypt it with backend) try { - const messageObject = JSON.parse(dataToUse.encryptedMessage); + const messageObject = JSON.parse(dataToUse.encryptedMessage) as ISubmissionMessageObject; decryptedPart = messageObject.decrypted ?? ""; encryptedPart = messageObject.encrypted ?? ""; + // isDecryptedPartEncryptedByHats = messageObject.isEncryptedByHats ?? false; } catch (error) { encryptedPart = dataToUse.encryptedMessage; } @@ -73,6 +77,13 @@ export const DecryptionPage = () => { decryptionKeys: privateKey, }); + // TODO: private audits V2 + // // If user could decrypt encrypted part (with committee key), lets decrypt the encryptedByHats part + // if (isDecryptedPartEncryptedByHats) { + // const decryptedPartByHats = await decryptUsingHatsKey(decryptedPart); + // decryptedPart = decryptedPartByHats; + // } + const decryptedMessage = (decrypted as string) + (decryptedPart ? `\n\n${decryptedPart}` : ""); setValue("decryptedMessage", decryptedMessage); diff --git a/packages/web/src/pages/CommitteeTools/DecryptionTool/DecryptionPage/decryptionService.api.ts b/packages/web/src/pages/CommitteeTools/DecryptionTool/DecryptionPage/decryptionService.api.ts new file mode 100644 index 000000000..6fda64af0 --- /dev/null +++ b/packages/web/src/pages/CommitteeTools/DecryptionTool/DecryptionPage/decryptionService.api.ts @@ -0,0 +1,3 @@ +export const decryptUsingHatsKey = async (encryptedData: string): Promise => { + throw new Error("Not implemented"); +}; diff --git a/packages/web/src/pages/CommitteeTools/SubmissionsTool/SubmissionDetailsPage/SubmissionDetailsPage.tsx b/packages/web/src/pages/CommitteeTools/SubmissionsTool/SubmissionDetailsPage/SubmissionDetailsPage.tsx index b72c9f97f..13850f1da 100644 --- a/packages/web/src/pages/CommitteeTools/SubmissionsTool/SubmissionDetailsPage/SubmissionDetailsPage.tsx +++ b/packages/web/src/pages/CommitteeTools/SubmissionsTool/SubmissionDetailsPage/SubmissionDetailsPage.tsx @@ -1,9 +1,10 @@ +import { allowedElementsMarkdown } from "@hats-finance/shared"; import ArrowBackIcon from "@mui/icons-material/ArrowBackIosNewOutlined"; import LinkIcon from "@mui/icons-material/InsertLinkOutlined"; import MDEditor from "@uiw/react-md-editor"; import { Alert, Button, HatSpinner, WalletButton } from "components"; import { useKeystore } from "components/Keystore"; -import { IPFS_PREFIX, disallowedElementsMarkdown } from "constants/constants"; +import { IPFS_PREFIX } from "constants/constants"; import { RoutePaths } from "navigation"; import { useEffect } from "react"; import { useTranslation } from "react-i18next"; @@ -84,7 +85,7 @@ export const SubmissionDetailsPage = () => { diff --git a/packages/web/src/pages/CommitteeTools/SubmissionsTool/submissionsService.api.ts b/packages/web/src/pages/CommitteeTools/SubmissionsTool/submissionsService.api.ts index 3e0f5e2c4..dfec0a9f0 100644 --- a/packages/web/src/pages/CommitteeTools/SubmissionsTool/submissionsService.api.ts +++ b/packages/web/src/pages/CommitteeTools/SubmissionsTool/submissionsService.api.ts @@ -24,11 +24,12 @@ export const getVaultSubmissionsByKeystore = async ( let decryptedPart = ""; let encryptedPart = ""; + //TODO: private audits V2 (verify is decryptedPart is encrypted with hats public key) if (typeof submission.submissionData === "object") { - decryptedPart = (submission.submissionData as any).decrypted ?? ""; - encryptedPart = (submission.submissionData as any).encrypted ?? ""; + decryptedPart = submission.submissionData.decrypted ?? ""; + encryptedPart = submission.submissionData.encrypted ?? ""; } else { - encryptedPart = submission.submissionData; + encryptedPart = submission.submissionData as string; } // Iterate over all stored keys and try to decrypt the message diff --git a/packages/web/src/pages/Honeypots/VaultDetailsPage/Sections/VaultRewardsSection/VaultRewardsSection.tsx b/packages/web/src/pages/Honeypots/VaultDetailsPage/Sections/VaultRewardsSection/VaultRewardsSection.tsx index 804ba1d97..0fafbc1b0 100644 --- a/packages/web/src/pages/Honeypots/VaultDetailsPage/Sections/VaultRewardsSection/VaultRewardsSection.tsx +++ b/packages/web/src/pages/Honeypots/VaultDetailsPage/Sections/VaultRewardsSection/VaultRewardsSection.tsx @@ -14,25 +14,27 @@ type VaultRewardsSectionProps = { export const VaultRewardsSection = ({ vault }: VaultRewardsSectionProps) => { const { t } = useTranslation(); - const isAudit = vault.description && vault.description["project-metadata"].type === "audit"; + const isAudit = vault.description && vault.description["project-metadata"].type === "audit"; const showIntended = vault.amountsInfo?.showCompetitionIntendedAmount ?? false; return ( - +

{t("rewards")}

-
-

{showIntended ? t("intendedDeposits") : t("totalDeposits")}

- {showIntended ? ( - -

~${millify(vault.amountsInfo?.competitionIntendedAmount?.deposited.usd ?? 0)}

-
- ) : ( -

~${millify(vault.amountsInfo?.depositedAmount.usd ?? 0)}

- )} -
+ {!isAudit && ( +
+

{showIntended ? t("intendedDeposits") : t("totalDeposits")}

+ {showIntended ? ( + +

~${millify(vault.amountsInfo?.competitionIntendedAmount?.deposited.usd ?? 0)}

+
+ ) : ( +

~${millify(vault.amountsInfo?.depositedAmount.usd ?? 0)}

+ )} +
+ )}

{t("assetsInVault")}

@@ -44,25 +46,20 @@ export const VaultRewardsSection = ({ vault }: VaultRewardsSectionProps) => {

~${millify(vault.amountsInfo?.competitionIntendedAmount?.maxReward.usd ?? 0)}

) : ( - // TODO: In here should be only the max reward amount, not the deposited amount - // Change this once we have the new contract version -

- ~$ - {isAudit - ? millify(vault.amountsInfo?.depositedAmount.usd ?? 0) - : millify(vault.amountsInfo?.maxRewardAmount.usd ?? 0)} -

+

~${millify(vault.amountsInfo?.maxRewardAmount.usd ?? 0)}

)}
-
-
-

{t("rewardsDivision")}

-
- + {!isAudit && ( +
+
+

{t("rewardsDivision")}

+
+ +
-
+ )}

{t("severityRewards")}

diff --git a/packages/web/src/pages/Honeypots/VaultDetailsPage/Sections/VaultRewardsSection/styles.ts b/packages/web/src/pages/Honeypots/VaultDetailsPage/Sections/VaultRewardsSection/styles.ts index 0bd940981..abcad1217 100644 --- a/packages/web/src/pages/Honeypots/VaultDetailsPage/Sections/VaultRewardsSection/styles.ts +++ b/packages/web/src/pages/Honeypots/VaultDetailsPage/Sections/VaultRewardsSection/styles.ts @@ -2,22 +2,37 @@ import styled, { css } from "styled-components"; import { getSpacing } from "styles"; import { breakpointsDefinition } from "styles/breakpoints.styles"; -export const StyledRewardsSection = styled.div<{ showIntended: boolean }>( - ({ showIntended }) => css` +export const StyledRewardsSection = styled.div<{ showIntended: boolean; isAudit: boolean }>( + ({ showIntended, isAudit }) => css` padding-bottom: ${getSpacing(10)}; .rewards-containers { display: grid; grid-template-columns: auto 1fr 1fr; - gap: ${getSpacing(1.5)}; + gap: ${getSpacing(3)}; flex-grow: 1; + ${isAudit && + css` + grid-template-columns: 1fr 3fr; + `} + @media (max-width: ${breakpointsDefinition.mediumScreen}) { grid-template-columns: auto 4fr 5fr; + + ${isAudit && + css` + grid-template-columns: 1fr 2fr; + `} } @media (max-width: ${breakpointsDefinition.mediumMobile}) { grid-template-columns: 1fr 1fr; + + ${isAudit && + css` + grid-template-columns: 1fr; + `} } @media (max-width: ${breakpointsDefinition.smallMobile}) { @@ -27,7 +42,7 @@ export const StyledRewardsSection = styled.div<{ showIntended: boolean }>( .amounts { display: grid; grid-template-columns: 1fr; - gap: ${getSpacing(1.5)}; + gap: ${getSpacing(3)}; @media (max-width: ${breakpointsDefinition.mediumMobile}) { grid-template-columns: 1fr; @@ -44,10 +59,11 @@ export const StyledRewardsSection = styled.div<{ showIntended: boolean }>( } .severities-rewards { - max-height: 500px; + max-height: 550px; overflow: hidden; @media (max-width: ${breakpointsDefinition.smallMobile}) { + max-height: unset; grid-column-start: 1; grid-column-end: 3; } diff --git a/packages/web/src/pages/Honeypots/VaultDetailsPage/Sections/VaultScopeSection/EnvSetupSection/EnvSetupSection.tsx b/packages/web/src/pages/Honeypots/VaultDetailsPage/Sections/VaultScopeSection/EnvSetupSection/EnvSetupSection.tsx index 02b84db18..855e4ec41 100644 --- a/packages/web/src/pages/Honeypots/VaultDetailsPage/Sections/VaultScopeSection/EnvSetupSection/EnvSetupSection.tsx +++ b/packages/web/src/pages/Honeypots/VaultDetailsPage/Sections/VaultScopeSection/EnvSetupSection/EnvSetupSection.tsx @@ -1,7 +1,6 @@ -import { IVault } from "@hats-finance/shared"; +import { IVault, allowedElementsMarkdown } from "@hats-finance/shared"; import MDEditor from "@uiw/react-md-editor"; import { Pill } from "components"; -import { disallowedElementsMarkdown } from "constants/constants"; import { useTranslation } from "react-i18next"; type EnvSetupSectionProps = { @@ -20,7 +19,7 @@ export const EnvSetupSection = ({ vault }: EnvSetupSectionProps) => {
diff --git a/packages/web/src/pages/Honeypots/VaultDetailsPage/Sections/VaultScopeSection/InScopeSection/InScopeSection.tsx b/packages/web/src/pages/Honeypots/VaultDetailsPage/Sections/VaultScopeSection/InScopeSection/InScopeSection.tsx index 12ea062e1..3e4bf704b 100644 --- a/packages/web/src/pages/Honeypots/VaultDetailsPage/Sections/VaultScopeSection/InScopeSection/InScopeSection.tsx +++ b/packages/web/src/pages/Honeypots/VaultDetailsPage/Sections/VaultScopeSection/InScopeSection/InScopeSection.tsx @@ -3,6 +3,7 @@ import { IEditedContractCovered, IVault, IVaultRepoInformation, + allowedElementsMarkdown, severitiesToContractsCoveredForm, } from "@hats-finance/shared"; import ArrowForwardIcon from "@mui/icons-material/ArrowForward"; @@ -13,9 +14,10 @@ import TerminalIcon from "@mui/icons-material/Terminal"; import ContractsIcon from "@mui/icons-material/ViewInAr"; import ContractsTwoIcon from "@mui/icons-material/ViewWeekOutlined"; import MDEditor from "@uiw/react-md-editor"; -import { Alert, Button, CopyToClipboard, Pill, WithTooltip } from "components"; -import { disallowedElementsMarkdown } from "constants/constants"; +import { Alert, Button, CopyToClipboard, Loading, Pill, WithTooltip } from "components"; import { defaultAnchorProps } from "constants/defaultAnchorProps"; +import useConfirm from "hooks/useConfirm"; +import { useVaultRepoName } from "pages/Honeypots/VaultDetailsPage/hooks"; import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { shortenIfAddress } from "utils/addresses.utils"; @@ -29,6 +31,9 @@ type InScopeSectionProps = { export const InScopeSection = ({ vault }: InScopeSectionProps) => { const { t } = useTranslation(); + const confirm = useConfirm(); + + const { data: repoName } = useVaultRepoName(vault); const [repoContractsList, setRepoContractsList] = useState([]); @@ -50,11 +55,29 @@ export const InScopeSection = ({ vault }: InScopeSectionProps) => { getRepoContractsList(); }, [vault.description?.scope?.reposInformation]); - if (!vault.description) return null; + if (!vault.description) return ; + const isPrivateAudit = vault?.description?.["project-metadata"].isPrivateAudit; const contractsCovered = severitiesToContractsCoveredForm(vault.description?.severities); const docsLink = vault.description.scope?.docsLink; + const goToGithubIssuesPrivateAudit = async () => { + if (!repoName) return; + + const githubLink = `https://github.com/hats-finance/${repoName}/issues`; + + const wantToGo = await confirm({ + title: t("openGithub"), + titleIcon: , + description: t("doYouWantToSeeSubmissionsOnGithub"), + cancelText: t("no"), + confirmText: t("yesGo"), + }); + + if (!wantToGo) return; + window.open(githubLink, "_blank"); + }; + const goToRepo = (repo: IVaultRepoInformation) => { if (repo.commitHash) { window.open(`${repo.url}/commit/${repo.commitHash}`, "_blank"); @@ -161,7 +184,7 @@ export const InScopeSection = ({ vault }: InScopeSectionProps) => {
@@ -176,32 +199,53 @@ export const InScopeSection = ({ vault }: InScopeSectionProps) => { )} - {/* Repos information */} - {vault.description.scope?.reposInformation && vault.description.scope?.reposInformation.length > 0 && ( + {isPrivateAudit ? ( <>

{t("repositories")}

-
- {vault.description.scope?.reposInformation.map((repo, idx) => ( -
-
-

{repo.url}

- {repo.commitHash && ( -

- {t("commitHash")}: {repo.commitHash} -

- )} -
- +
+
+

{`https://github.com/hats-finance/${repoName}`}

- ))} + +
- {(contractsCovered.length > 0 || repoContractsList.length > 0) &&
} + + ) : ( + <> + {/* Repos information */} + {vault.description.scope?.reposInformation && vault.description.scope?.reposInformation.length > 0 && ( + <> +

+ + {t("repositories")} +

+ +
+ {vault.description.scope?.reposInformation.map((repo, idx) => ( +
+
+

{repo.url}

+ {repo.commitHash && ( +

+ {t("commitHash")}: {repo.commitHash} +

+ )} +
+ +
+ ))} +
+ {(contractsCovered.length > 0 || repoContractsList.length > 0) &&
} + + )} )} @@ -219,7 +263,9 @@ export const InScopeSection = ({ vault }: InScopeSectionProps) => { ) ) : ( - {t("loadingContractsOnRepo")} + + {t("loadingContractsOnRepo")} + )} {/* Contracts covered */} diff --git a/packages/web/src/pages/Honeypots/VaultDetailsPage/Sections/VaultScopeSection/OutOfScopeSection/OutOfScopeSection.tsx b/packages/web/src/pages/Honeypots/VaultDetailsPage/Sections/VaultScopeSection/OutOfScopeSection/OutOfScopeSection.tsx index 4e8df8f40..a04702610 100644 --- a/packages/web/src/pages/Honeypots/VaultDetailsPage/Sections/VaultScopeSection/OutOfScopeSection/OutOfScopeSection.tsx +++ b/packages/web/src/pages/Honeypots/VaultDetailsPage/Sections/VaultScopeSection/OutOfScopeSection/OutOfScopeSection.tsx @@ -1,6 +1,5 @@ -import { IVault } from "@hats-finance/shared"; +import { IVault, allowedElementsMarkdown } from "@hats-finance/shared"; import MDEditor from "@uiw/react-md-editor"; -import { disallowedElementsMarkdown } from "constants/constants"; type OutOfScopeSectionProps = { vault: IVault; @@ -12,7 +11,7 @@ export const OutOfScopeSection = ({ vault }: OutOfScopeSectionProps) => { return (
diff --git a/packages/web/src/pages/Honeypots/VaultDetailsPage/Sections/VaultScopeSection/SeverityLevelsSection/SeverityLevelsSection.tsx b/packages/web/src/pages/Honeypots/VaultDetailsPage/Sections/VaultScopeSection/SeverityLevelsSection/SeverityLevelsSection.tsx index 15b7391b1..f0a3cdbad 100644 --- a/packages/web/src/pages/Honeypots/VaultDetailsPage/Sections/VaultScopeSection/SeverityLevelsSection/SeverityLevelsSection.tsx +++ b/packages/web/src/pages/Honeypots/VaultDetailsPage/Sections/VaultScopeSection/SeverityLevelsSection/SeverityLevelsSection.tsx @@ -1,7 +1,6 @@ -import { IVault, IVulnerabilitySeverity } from "@hats-finance/shared"; +import { IVault, IVulnerabilitySeverity, allowedElementsMarkdown } from "@hats-finance/shared"; import MDEditor from "@uiw/react-md-editor"; import { Pill } from "components"; -import { disallowedElementsMarkdown } from "constants/constants"; import { getSeveritiesColorsArray } from "hooks/severities/useSeverityRewardInfo"; import { StyledSeverityLevelsSection } from "./styles"; @@ -20,7 +19,7 @@ export const SeverityLevelsSection = ({ vault }: SeverityLevelsSectionProps) =>
{ const { t } = useTranslation(); + const isPrivateAudit = vault?.description?.["project-metadata"].isPrivateAudit; + return ( + {isPrivateAudit && ( + + {t("privateAuditSubmissionsScopeInGithub")} + + )} +

{t("inScope")}

diff --git a/packages/web/src/pages/Honeypots/VaultDetailsPage/Sections/VaultSubmissionsSection/PublicSubmissionCard/PublicSubmissionCard.tsx b/packages/web/src/pages/Honeypots/VaultDetailsPage/Sections/VaultSubmissionsSection/PublicSubmissionCard/PublicSubmissionCard.tsx new file mode 100644 index 000000000..ea229e9db --- /dev/null +++ b/packages/web/src/pages/Honeypots/VaultDetailsPage/Sections/VaultSubmissionsSection/PublicSubmissionCard/PublicSubmissionCard.tsx @@ -0,0 +1,51 @@ +import { IVault, IVulnerabilitySeverity, allowedElementsMarkdown } from "@hats-finance/shared"; +import MDEditor from "@uiw/react-md-editor"; +import { Pill } from "components"; +import { getSeveritiesColorsArray } from "hooks/severities/useSeverityRewardInfo"; +import moment from "moment"; +import { IGithubIssue } from "pages/Honeypots/VaultDetailsPage/types"; +import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { StyledPublicSubmissionCard } from "./styles"; + +type PublicSubmissionCardProps = { + vault: IVault; + submission: IGithubIssue; +}; + +function PublicSubmissionCard({ vault, submission }: PublicSubmissionCardProps) { + const { t } = useTranslation(); + + const [isOpen, setIsOpen] = useState(false); + + const severityColors = getSeveritiesColorsArray(vault); + const severityIndex = + submission.severity && + vault?.description?.severities.findIndex((sev: IVulnerabilitySeverity) => + sev.name.toLowerCase().includes(submission.severity ?? "") + ); + + return ( + +
setIsOpen((prev) => !prev)}> +
+ +
+

{moment(submission.createdAt).format("Do MMM YYYY - hh:mma")}

+

{submission.issueData.issueTitle}

+
+ +
+ +
+
+ ); +} + +export default PublicSubmissionCard; diff --git a/packages/web/src/pages/Honeypots/VaultDetailsPage/Sections/VaultSubmissionsSection/PublicSubmissionCard/styles.ts b/packages/web/src/pages/Honeypots/VaultDetailsPage/Sections/VaultSubmissionsSection/PublicSubmissionCard/styles.ts new file mode 100644 index 000000000..965585cfe --- /dev/null +++ b/packages/web/src/pages/Honeypots/VaultDetailsPage/Sections/VaultSubmissionsSection/PublicSubmissionCard/styles.ts @@ -0,0 +1,56 @@ +import styled, { css } from "styled-components"; +import { getSpacing } from "styles"; + +export const StyledPublicSubmissionCard = styled.div<{ isOpen: boolean }>( + ({ isOpen }) => css` + position: relative; + + .card-header { + position: relative; + display: flex; + flex-direction: column; + gap: ${getSpacing(1.5)}; + border: 1px solid var(--primary-light); + padding: ${getSpacing(3)} ${getSpacing(4)}; + background: var(--background-2); + cursor: pointer; + transition: 0.2s; + + &:hover { + border-color: var(--primary); + } + + .date { + position: absolute; + top: ${getSpacing(3)}; + right: ${getSpacing(4)}; + } + + .submission-title { + font-size: var(--small); + padding-left: ${getSpacing(1)}; + font-weight: 700; + } + } + + .card-content { + overflow: hidden; + height: 0; + + ${isOpen && + css` + height: auto; + `} + + .submission-content { + white-space: normal; + font-size: var(--xsmall); + background: var(--background-3); + padding: ${getSpacing(3)} ${getSpacing(4)}; + color: var(--white); + border: 1px solid var(--primary-light); + border-top: none; + } + } + ` +); diff --git a/packages/web/src/pages/Honeypots/VaultDetailsPage/Sections/VaultSubmissionsSection/VaultSubmissionsSection.tsx b/packages/web/src/pages/Honeypots/VaultDetailsPage/Sections/VaultSubmissionsSection/VaultSubmissionsSection.tsx new file mode 100644 index 000000000..48fe4a0ba --- /dev/null +++ b/packages/web/src/pages/Honeypots/VaultDetailsPage/Sections/VaultSubmissionsSection/VaultSubmissionsSection.tsx @@ -0,0 +1,74 @@ +import { IVault } from "@hats-finance/shared"; +import OpenIcon from "@mui/icons-material/OpenInNewOutlined"; +import { Alert, Button, HatSpinner } from "components"; +import useConfirm from "hooks/useConfirm"; +import { useTranslation } from "react-i18next"; +import { useSavedSubmissions, useVaultRepoName } from "../../hooks"; +import PublicSubmissionCard from "./PublicSubmissionCard/PublicSubmissionCard"; +import { StyledSubmissionsSection } from "./styles"; + +type VaultSubmissionsSectionProps = { + vault: IVault; + noDeployed?: boolean; +}; + +export const VaultSubmissionsSection = ({ vault }: VaultSubmissionsSectionProps) => { + const { t } = useTranslation(); + const confirm = useConfirm(); + + const { data: savedSubmissions, isLoading } = useSavedSubmissions(vault); + const { data: repoName } = useVaultRepoName(vault); + const isPrivateAudit = vault?.description?.["project-metadata"].isPrivateAudit; + + const goToGithubIssues = async () => { + if (!repoName) return; + + const githubLink = `https://github.com/hats-finance/${repoName}/issues`; + + const wantToGo = await confirm({ + title: t("openGithub"), + titleIcon: , + description: t("doYouWantToSeeSubmissionsOnGithub"), + cancelText: t("no"), + confirmText: t("yesGo"), + }); + + if (!wantToGo) return; + window.open(githubLink, "_blank"); + }; + + return ( + + {isPrivateAudit && ( + + {t("privateAuditSubmissionsOnlyOnGithub")} + + )} +

+ {t("submissions")} + + {repoName && ( + + )} +

+ + {!isPrivateAudit && savedSubmissions?.length === 0 && ( + + {t("thereIsNoPublicSubmission")} + + )} + + {!isPrivateAudit && isLoading && } + + {!isPrivateAudit && savedSubmissions && savedSubmissions?.length > 0 && ( +
+ {savedSubmissions.map((submission) => ( + + ))} +
+ )} +
+ ); +}; diff --git a/packages/web/src/pages/Honeypots/VaultDetailsPage/Sections/VaultSubmissionsSection/styles.ts b/packages/web/src/pages/Honeypots/VaultDetailsPage/Sections/VaultSubmissionsSection/styles.ts new file mode 100644 index 000000000..08171b6ac --- /dev/null +++ b/packages/web/src/pages/Honeypots/VaultDetailsPage/Sections/VaultSubmissionsSection/styles.ts @@ -0,0 +1,13 @@ +import styled from "styled-components"; +import { getSpacing } from "styles"; + +export const StyledSubmissionsSection = styled.div` + padding-bottom: ${getSpacing(10)}; + + .public-submissions { + margin-top: ${getSpacing(3)}; + display: flex; + flex-direction: column; + gap: ${getSpacing(3)}; + } +`; diff --git a/packages/web/src/pages/Honeypots/VaultDetailsPage/Sections/index.ts b/packages/web/src/pages/Honeypots/VaultDetailsPage/Sections/index.ts index b319b3e43..1d3c85d41 100644 --- a/packages/web/src/pages/Honeypots/VaultDetailsPage/Sections/index.ts +++ b/packages/web/src/pages/Honeypots/VaultDetailsPage/Sections/index.ts @@ -1,3 +1,4 @@ export { VaultRewardsSection } from "./VaultRewardsSection/VaultRewardsSection"; export { VaultScopeSection } from "./VaultScopeSection/VaultScopeSection"; export { VaultDepositsSection } from "./VaultDepositsSection/VaultDepositsSection"; +export { VaultSubmissionsSection } from "./VaultSubmissionsSection/VaultSubmissionsSection"; diff --git a/packages/web/src/pages/Honeypots/VaultDetailsPage/VaultDetailsPage.tsx b/packages/web/src/pages/Honeypots/VaultDetailsPage/VaultDetailsPage.tsx index 6473a1ec6..f17f80569 100644 --- a/packages/web/src/pages/Honeypots/VaultDetailsPage/VaultDetailsPage.tsx +++ b/packages/web/src/pages/Honeypots/VaultDetailsPage/VaultDetailsPage.tsx @@ -1,11 +1,11 @@ import { IVault } from "@hats-finance/shared"; import { Alert, Loading, Seo, VaultCard } from "components"; import { useVaults } from "hooks/subgraph/vaults/useVaults"; -import { useState } from "react"; +import { useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; -import { redirect, useNavigate, useParams } from "react-router-dom"; +import { useNavigate, useParams } from "react-router-dom"; import { HoneypotsRoutePaths } from "../router"; -import { VaultDepositsSection, VaultRewardsSection, VaultScopeSection } from "./Sections"; +import { VaultDepositsSection, VaultRewardsSection, VaultScopeSection, VaultSubmissionsSection } from "./Sections"; import { StyledSectionTab, StyledVaultDetailsPage } from "./styles"; const DETAILS_SECTIONS = [ @@ -21,6 +21,10 @@ const DETAILS_SECTIONS = [ title: "deposits", component: VaultDepositsSection, }, + { + title: "submissions", + component: VaultSubmissionsSection, + }, ]; type VaultDetailsPageProps = { @@ -37,17 +41,26 @@ export const VaultDetailsPage = ({ vaultToUse, noActions = false, noDeployed = f const { vaultSlug, sectionId } = useParams(); const vaultId = vaultSlug?.split("-").pop(); const vault = vaultToUse ?? allVaults?.find((vault) => vault.id === vaultId); + const isAudit = vault?.description?.["project-metadata"].type === "audit"; const [openSectionId, setOpenSectionId] = useState(sectionId ?? DETAILS_SECTIONS[0].title); + const DETAILS_SECTIONS_TO_SHOW = useMemo( + () => + DETAILS_SECTIONS.filter((section) => { + if (section.title === "deposits" && noActions) return false; + if (section.title === "submissions" && !isAudit) return false; + return true; + }), + [noActions, isAudit] + ); + if (allVaults?.length === 0) return ; if (!vault || !vault.description) { - redirect("/"); - return null; + return ; } const activeClaim = vault.activeClaim; - const isAudit = vault.description["project-metadata"].type === "audit"; const vaultName = vault.description["project-metadata"].name; const navigateToType = () => { @@ -70,7 +83,7 @@ export const VaultDetailsPage = ({ vaultToUse, noActions = false, noDeployed = f return ( <> - + {!noActions && (
@@ -91,7 +104,7 @@ export const VaultDetailsPage = ({ vaultToUse, noActions = false, noDeployed = f
- {DETAILS_SECTIONS.filter((section) => (noActions ? section.title !== "deposits" : true)).map((section) => ( + {DETAILS_SECTIONS_TO_SHOW.map((section) => ( changeDetailsSection(section.title)} active={openSectionId === section.title} diff --git a/packages/web/src/pages/Honeypots/VaultDetailsPage/hooks.ts b/packages/web/src/pages/Honeypots/VaultDetailsPage/hooks.ts new file mode 100644 index 000000000..a411cc190 --- /dev/null +++ b/packages/web/src/pages/Honeypots/VaultDetailsPage/hooks.ts @@ -0,0 +1,28 @@ +import { IVault } from "@hats-finance/shared"; +import { UseQueryResult, useQuery } from "@tanstack/react-query"; +import * as savedSubmissionsService from "./savedSubmissionsService"; +import { IGithubIssue } from "./types"; + +/** + * Returns all saved submissions for a vault + */ +export const useSavedSubmissions = (vault: IVault | undefined): UseQueryResult => { + return useQuery({ + queryKey: ["github-issues", vault?.id], + queryFn: () => savedSubmissionsService.getSavedSubmissions(vault?.id), + refetchOnWindowFocus: false, + enabled: !!vault, + }); +}; + +/** + * Returns the repo name created for a vault + */ +export const useVaultRepoName = (vault: IVault | undefined): UseQueryResult => { + return useQuery({ + queryKey: ["github-repo-name", vault?.id], + queryFn: () => savedSubmissionsService.getVaultRepoName(vault?.id), + refetchOnWindowFocus: false, + enabled: !!vault, + }); +}; diff --git a/packages/web/src/pages/Honeypots/VaultDetailsPage/savedSubmissionsService.ts b/packages/web/src/pages/Honeypots/VaultDetailsPage/savedSubmissionsService.ts new file mode 100644 index 000000000..7ef3c42f7 --- /dev/null +++ b/packages/web/src/pages/Honeypots/VaultDetailsPage/savedSubmissionsService.ts @@ -0,0 +1,43 @@ +import { axiosClient } from "config/axiosClient"; +import { BASE_SERVICE_URL } from "settings"; +import { IGithubIssue } from "./types"; + +/** + * Get all saved github issues for a vault + */ +export async function getSavedSubmissions(vaultId: string | undefined): Promise { + if (!vaultId) return []; + + try { + const response = await axiosClient.get(`${BASE_SERVICE_URL}/github-repos/issues/${vaultId}`); + const githubIssues = response.data.githubIssues as IGithubIssue[]; + const githubIssuesWithSeverity = githubIssues.map((issue) => { + const severity = issue.issueData.issueDescription.match(/(\*\*Severity:\*\* (.*)\n)/)?.[2]; + return { + ...issue, + severity, + }; + }); + + return githubIssuesWithSeverity; + } catch (error) { + return []; + } +} + +/** + * Get the repo name created for a vault + */ +export async function getVaultRepoName(vaultId: string | undefined): Promise { + if (!vaultId) return undefined; + + try { + const response = await axiosClient.get(`${BASE_SERVICE_URL}/github-repos/repo/${vaultId}`); + const repoName = response.data.repoName as string | undefined; + console.log(response); + + return repoName; + } catch (error) { + return undefined; + } +} diff --git a/packages/web/src/pages/Honeypots/VaultDetailsPage/styles.ts b/packages/web/src/pages/Honeypots/VaultDetailsPage/styles.ts index a6da604ff..e8243f6e2 100644 --- a/packages/web/src/pages/Honeypots/VaultDetailsPage/styles.ts +++ b/packages/web/src/pages/Honeypots/VaultDetailsPage/styles.ts @@ -2,8 +2,8 @@ import styled, { css } from "styled-components"; import { getSpacing } from "styles"; import { breakpointsDefinition } from "styles/breakpoints.styles"; -export const StyledVaultDetailsPage = styled.div<{ isAudit: boolean }>( - ({ isAudit }) => css` +export const StyledVaultDetailsPage = styled.div<{ isAudit: boolean; tabsNumber: number }>( + ({ isAudit, tabsNumber }) => css` .breadcrumb { span.type { color: var(--grey-500); @@ -26,7 +26,7 @@ export const StyledVaultDetailsPage = styled.div<{ isAudit: boolean }>( padding-top: ${getSpacing(6)}; margin-top: ${getSpacing(4)}; display: grid; - grid-template-columns: repeat(3, 1fr); + grid-template-columns: repeat(${tabsNumber}, 1fr); gap: ${getSpacing(2)}; overflow-x: auto; overflow-y: hidden; diff --git a/packages/web/src/pages/Honeypots/VaultDetailsPage/types.ts b/packages/web/src/pages/Honeypots/VaultDetailsPage/types.ts new file mode 100644 index 000000000..a79395179 --- /dev/null +++ b/packages/web/src/pages/Honeypots/VaultDetailsPage/types.ts @@ -0,0 +1,10 @@ +import { ISubmitSubmissionRequest } from "pages/Submissions/SubmissionFormPage/types"; + +export interface IGithubIssue { + _id: string; + createdAt: Date; + vaultId: string; + repoName: string; + issueData: ISubmitSubmissionRequest["createIssueRequests"][0]; + severity?: string; +} diff --git a/packages/web/src/pages/Honeypots/VaultsPage/AuditVaultsPage.tsx b/packages/web/src/pages/Honeypots/VaultsPage/AuditVaultsPage.tsx index cf22cccee..576e65c12 100644 --- a/packages/web/src/pages/Honeypots/VaultsPage/AuditVaultsPage.tsx +++ b/packages/web/src/pages/Honeypots/VaultsPage/AuditVaultsPage.tsx @@ -1,10 +1,12 @@ import { Seo, VaultAuditDraftCard, VaultCard, VaultCardSkeleton } from "components"; +import { useVaults } from "hooks/subgraph/vaults/useVaults"; import { useTranslation } from "react-i18next"; import { useAuditCompetitionsVaults, useDraftAuditCompetitions, useOldAuditCompetitions } from "./hooks"; import { StyledVaultsPage } from "./styles"; export const AuditVaultsPage = () => { const { t } = useTranslation(); + const { vaultsReadyAllChains } = useVaults(); const { live: liveAuditCompetitions, @@ -17,14 +19,11 @@ export const AuditVaultsPage = () => { const draftAudits = useDraftAuditCompetitions(); - const areVaultsToShow = - liveAuditCompetitions.length > 0 || upcomingAuditCompetitions.length > 0 || finishedAuditPayouts.length > 0; - return ( <> - {!areVaultsToShow && ( + {!vaultsReadyAllChains && (

{t("liveCompetitions")}

diff --git a/packages/web/src/pages/Honeypots/VaultsPage/BugBountyVaultsPage.tsx b/packages/web/src/pages/Honeypots/VaultsPage/BugBountyVaultsPage.tsx index 764d4f8db..a844621a8 100644 --- a/packages/web/src/pages/Honeypots/VaultsPage/BugBountyVaultsPage.tsx +++ b/packages/web/src/pages/Honeypots/VaultsPage/BugBountyVaultsPage.tsx @@ -1,23 +1,23 @@ import { Pill, Seo, VaultAuditDraftCard, VaultCard, VaultCardSkeleton } from "components"; +import { useVaults } from "hooks/subgraph/vaults/useVaults"; import { useTranslation } from "react-i18next"; import { useAuditCompetitionsVaults, useBugBountiesVaults, useDraftAuditCompetitions } from "./hooks"; import { StyledVaultsPage } from "./styles"; export const BugBountyVaultsPage = () => { const { t } = useTranslation(); + const { vaultsReadyAllChains } = useVaults(); const { live: liveAuditCompetitions, upcoming: upcomingAuditCompetitions } = useAuditCompetitionsVaults(); const bugBounties = useBugBountiesVaults(); const draftAudits = useDraftAuditCompetitions(); - const areVaultsToShow = liveAuditCompetitions.length > 0 || upcomingAuditCompetitions.length > 0 || bugBounties.length > 0; - return ( <> - {!areVaultsToShow && ( + {!vaultsReadyAllChains && (

{t("bugBounties")}

diff --git a/packages/web/src/pages/Honeypots/VaultsPage/PrivateAuditVaultsPage.tsx b/packages/web/src/pages/Honeypots/VaultsPage/PrivateAuditVaultsPage.tsx new file mode 100644 index 000000000..7799f71c3 --- /dev/null +++ b/packages/web/src/pages/Honeypots/VaultsPage/PrivateAuditVaultsPage.tsx @@ -0,0 +1,96 @@ +import { Alert, Button, Seo, VaultCard, VaultCardSkeleton } from "components"; +import { useSiweAuth } from "hooks/siwe/useSiweAuth"; +import { useVaults } from "hooks/subgraph/vaults/useVaults"; +import { useTranslation } from "react-i18next"; +import { useAuditCompetitionsVaults, useOldAuditCompetitions } from "./hooks"; +import { StyledVaultsPage } from "./styles"; + +export const PrivateAuditVaultsPage = () => { + const { t } = useTranslation(); + const { tryAuthentication, isAuthenticated } = useSiweAuth(); + const { vaultsReadyAllChains } = useVaults(); + + const { + live: livePrivAuditCompetitions, + upcoming: upcomingPrivAuditCompetitions, + finished: finishePrivAuditPayouts, + } = useAuditCompetitionsVaults({ private: true }); + + const oldAudits = useOldAuditCompetitions(); + const allFinishedAuditCompetitions = [...finishePrivAuditPayouts, ...(oldAudits ?? [])]; + + const areVaultsToShow = + livePrivAuditCompetitions.length > 0 || upcomingPrivAuditCompetitions.length > 0 || finishePrivAuditPayouts.length > 0; + + if (!isAuthenticated) { + return ( + + + {t("pleaseSignInWithEthereumToSeePrivateComps")} + + + + ); + } + + return ( + <> + + + {!vaultsReadyAllChains && ( +
+

{t("livePrivateCompetitions")}

+ +

{t("upcomingPrivateCompetitions")}

+ +

{t("finishedPrivateCompetitions")}

+ + + +
+ )} + + {!areVaultsToShow && ( + + {t("youAreNotInvitedToAnyPrivateComps")} + + )} + + {livePrivAuditCompetitions.length > 0 && ( + <> +

{t("livePrivateCompetitions")}

+
+ {livePrivAuditCompetitions.map((auditVault, idx) => ( + + ))} +
+ + )} + + {upcomingPrivAuditCompetitions.length > 0 && ( + <> +

{t("upcomingPrivateCompetitions")}

+
+ {upcomingPrivAuditCompetitions.map((auditVault, idx) => ( + + ))} +
+ + )} + + {allFinishedAuditCompetitions.length > 0 && ( + <> +

{t("finishedPrivateCompetitions")}

+
+ {allFinishedAuditCompetitions.map((auditPayout, idx) => ( + + ))} +
+ + )} +
+ + ); +}; diff --git a/packages/web/src/pages/Honeypots/VaultsPage/hooks.ts b/packages/web/src/pages/Honeypots/VaultsPage/hooks.ts index edfc52926..b7f9bf66b 100644 --- a/packages/web/src/pages/Honeypots/VaultsPage/hooks.ts +++ b/packages/web/src/pages/Honeypots/VaultsPage/hooks.ts @@ -1,27 +1,68 @@ -import { IEditedSessionResponse, IPayoutGraph } from "@hats-finance/shared"; +import { IEditedSessionResponse, IPayoutGraph, isAddressAMultisigMember } from "@hats-finance/shared"; import { useQuery } from "@tanstack/react-query"; import { axiosClient } from "config/axiosClient"; +import { useSiweAuth } from "hooks/siwe/useSiweAuth"; import { useVaults } from "hooks/subgraph/vaults/useVaults"; +import { useEffect, useState } from "react"; import { BASE_SERVICE_URL, IS_PROD, appChains } from "settings"; -import { useNetwork } from "wagmi"; +import { useAccount, useNetwork } from "wagmi"; import * as auditDraftsService from "./auditDraftsService"; /** - * Returns the live/upcoming/finished audit competitions + * Returns the live/upcoming/finished private audit competitions * * @remarks - * The finished competitions are gotten from the payouts. + * - The finished competitions are gotten from the payouts. + * - Only invited users or governance can access to private audits. */ -export const useAuditCompetitionsVaults = () => { +export const useAuditCompetitionsVaults = (opts: { private: boolean } = { private: false }) => { + const { address } = useAccount(); + const { chain } = useNetwork(); + const { tryAuthentication, profileData } = useSiweAuth(); const { allVaultsOnEnv, allPayoutsOnEnv } = useVaults(); + const [isGovMember, setIsGovMember] = useState(false); + + useEffect(() => { + if (opts.private) tryAuthentication(); + }, [tryAuthentication, opts.private]); + + useEffect(() => { + const checkGovMember = async () => { + if (address && chain && chain.id) { + const chainId = Number(chain.id); + const govMultisig = appChains[Number(chainId)]?.govMultisig; + + const isGov = await isAddressAMultisigMember(govMultisig, address, chainId); + setIsGovMember(isGov); + } + }; + checkGovMember(); + }, [address, chain]); + const auditCompetitionsVaults = allVaultsOnEnv ?.filter((vault) => vault.registered) - .filter((vault) => vault.description?.["project-metadata"].type === "audit") ?? []; - - const paidPayoutsFromAudits = allPayoutsOnEnv?.filter( - (payout) => payout.isApproved && payout.payoutData?.vault?.description?.["project-metadata"].type === "audit" - ); + .filter((vault) => vault.description?.["project-metadata"].type === "audit") + .filter((vault) => { + const isPrivateAudit = vault.description?.["project-metadata"].isPrivateAudit; + const isUserInvited = vault.description?.["project-metadata"].whitelist?.some( + (whiteAddress) => whiteAddress.address.toLowerCase() === profileData?.address?.toLowerCase() + ); + + return opts.private ? isPrivateAudit && (isUserInvited || isGovMember) : !isPrivateAudit; + }) ?? []; + + const paidPayoutsFromAudits = allPayoutsOnEnv + ?.filter((payout) => payout.isApproved) + .filter((payout) => payout.payoutData?.vault?.description?.["project-metadata"].type === "audit") + .filter((payout) => { + const isPrivateAudit = payout.payoutData?.vault?.description?.["project-metadata"].isPrivateAudit; + const isUserInvited = payout.payoutData?.vault?.description?.["project-metadata"].whitelist?.some( + (whiteAddress) => whiteAddress.address.toLowerCase() === profileData?.address?.toLowerCase() + ); + + return opts.private ? isPrivateAudit && (isUserInvited || isGovMember) : !isPrivateAudit; + }); auditCompetitionsVaults.sort((a, b) => (b.amountsInfo?.depositedAmount.usd ?? 0) - (a.amountsInfo?.depositedAmount.usd ?? 0)); diff --git a/packages/web/src/pages/Honeypots/router.tsx b/packages/web/src/pages/Honeypots/router.tsx index 3c2a46a76..8f1beb195 100644 --- a/packages/web/src/pages/Honeypots/router.tsx +++ b/packages/web/src/pages/Honeypots/router.tsx @@ -2,10 +2,12 @@ import { Navigate, RouteObject } from "react-router-dom"; import { VaultDetailsPage } from "./VaultDetailsPage/VaultDetailsPage"; import { AuditVaultsPage } from "./VaultsPage/AuditVaultsPage"; import { BugBountyVaultsPage } from "./VaultsPage/BugBountyVaultsPage"; +import { PrivateAuditVaultsPage } from "./VaultsPage/PrivateAuditVaultsPage"; export enum HoneypotsRoutePaths { bugBounties = "bug-bounties", audits = "audit-competitions", + privateAudits = "private-audit-competitions", } const vaultDetailsRoutes: RouteObject[] = [ @@ -47,5 +49,15 @@ export const honeypotsRouter = (): RouteObject => ({ ...vaultDetailsRoutes, ], }, + { + path: HoneypotsRoutePaths.privateAudits, + children: [ + { + path: "", + element: , + }, + ...vaultDetailsRoutes, + ], + }, ], }); diff --git a/packages/web/src/pages/Submissions/SubmissionFormPage/FormSteps/SubmissionDescriptions/SubmissionDescriptions.tsx b/packages/web/src/pages/Submissions/SubmissionFormPage/FormSteps/SubmissionDescriptions/SubmissionDescriptions.tsx index 446af0490..3df895729 100644 --- a/packages/web/src/pages/Submissions/SubmissionFormPage/FormSteps/SubmissionDescriptions/SubmissionDescriptions.tsx +++ b/packages/web/src/pages/Submissions/SubmissionFormPage/FormSteps/SubmissionDescriptions/SubmissionDescriptions.tsx @@ -1,4 +1,4 @@ -import { IVulnerabilitySeverity } from "@hats-finance/shared"; +import { ISubmissionMessageObject, IVulnerabilitySeverity } from "@hats-finance/shared"; import { yupResolver } from "@hookform/resolvers/yup"; import AddIcon from "@mui/icons-material/AddOutlined"; import RemoveIcon from "@mui/icons-material/DeleteOutlined"; @@ -17,7 +17,7 @@ import { getCustomIsDirty, useEnhancedForm } from "hooks/form"; import { useContext, useEffect, useState } from "react"; import { Controller, useFieldArray, useWatch } from "react-hook-form"; import { useTranslation } from "react-i18next"; -import { encryptWithKeys } from "../../encrypt"; +import { encryptWithHatsKey, encryptWithKeys } from "../../encrypt"; import { SUBMISSION_INIT_DATA, SubmissionFormContext } from "../../store"; import { ISubmissionsDescriptionsData } from "../../types"; import { getCreateDescriptionSchema } from "./formSchema"; @@ -31,6 +31,7 @@ export function SubmissionDescriptions() { const [severitiesOptions, setSeveritiesOptions] = useState(); const isAuditSubmission = vault?.description?.["project-metadata"].type === "audit"; + const isPrivateAudit = vault?.description?.["project-metadata"].isPrivateAudit; const { register, handleSubmit, control, reset, setValue } = useEnhancedForm({ resolver: yupResolver(getCreateDescriptionSchema(t)), @@ -123,11 +124,22 @@ export function SubmissionDescriptions() { if (!encryptionResult) return alert("This vault doesn't have any valid key, please contact hats team"); const { encryptedData, sessionKey } = encryptionResult; - const submissionInfo = { - ref: submissionData.ref, - decrypted, - encrypted: encryptedData as string, - }; + + let submissionInfo: ISubmissionMessageObject | undefined; + + try { + submissionInfo = { + ref: submissionData.ref, + isEncryptedByHats: isPrivateAudit, + decrypted: isPrivateAudit ? await encryptWithHatsKey(decrypted ?? "--Nothing decrypted--") : decrypted, + encrypted: encryptedData as string, + }; + } catch (error) { + console.log(error); + return alert("There was a problem encrypting the submission with Hats key. Please contact HATS team."); + } + + console.log(submissionInfo); download( JSON.stringify({ submission: submissionInfo, sessionKey }), diff --git a/packages/web/src/pages/Submissions/SubmissionFormPage/FormSteps/SubmissionDescriptions/utils.ts b/packages/web/src/pages/Submissions/SubmissionFormPage/FormSteps/SubmissionDescriptions/utils.ts index 98925a0e9..9b75135ba 100644 --- a/packages/web/src/pages/Submissions/SubmissionFormPage/FormSteps/SubmissionDescriptions/utils.ts +++ b/packages/web/src/pages/Submissions/SubmissionFormPage/FormSteps/SubmissionDescriptions/utils.ts @@ -1,3 +1,5 @@ +import { allowedAttributesMarkdown, allowedElementsMarkdown } from "@hats-finance/shared"; +import sanitizeMarkdown from "sanitize-markdown"; import { BASE_SERVICE_URL } from "settings"; import { ISubmissionData, ISubmissionsDescriptionsData } from "../../types"; @@ -58,7 +60,20 @@ ${ const submissionMessage = `\`\`\`\n> [ENCRYPTED SECTION]\n\`\`\`\n\n${toEncrypt}\n\n\n \`\`\`\n> [DECRYPTED SECTION]\n\`\`\`\n\n${decrypted}`; - return { decrypted, toEncrypt, submissionMessage }; + return { + decrypted: sanitizeMarkdown(decrypted, { + allowedTags: allowedElementsMarkdown, + allowedAttributes: allowedAttributesMarkdown, + }) as string, + toEncrypt: sanitizeMarkdown(toEncrypt, { + allowedTags: allowedElementsMarkdown, + allowedAttributes: allowedAttributesMarkdown, + }) as string, + submissionMessage: sanitizeMarkdown(submissionMessage, { + allowedTags: allowedElementsMarkdown, + allowedAttributes: allowedAttributesMarkdown, + }) as string, + }; }; export const getBountySubmissionTexts = ( @@ -81,7 +96,17 @@ ${description.description.trim()} const submissionMessage = `\`\`\`\n> [ENCRYPTED SECTION]\n\`\`\`\n\n${toEncrypt}`; - return { decrypted: undefined, toEncrypt, submissionMessage }; + return { + decrypted: undefined, + toEncrypt: sanitizeMarkdown(toEncrypt, { + allowedTags: allowedElementsMarkdown, + allowedAttributes: allowedAttributesMarkdown, + }) as string, + submissionMessage: sanitizeMarkdown(submissionMessage, { + allowedTags: allowedElementsMarkdown, + allowedAttributes: allowedAttributesMarkdown, + }) as string, + }; }; export const getGithubIssueDescription = ( diff --git a/packages/web/src/pages/Submissions/SubmissionFormPage/FormSteps/SubmissionProject/SubmissionProject.tsx b/packages/web/src/pages/Submissions/SubmissionFormPage/FormSteps/SubmissionProject/SubmissionProject.tsx index d8b444210..b5972d38f 100644 --- a/packages/web/src/pages/Submissions/SubmissionFormPage/FormSteps/SubmissionProject/SubmissionProject.tsx +++ b/packages/web/src/pages/Submissions/SubmissionFormPage/FormSteps/SubmissionProject/SubmissionProject.tsx @@ -30,6 +30,11 @@ export function SubmissionProject() { const vaultsProjects = activeVaults?.map((vault: IVault, index: number) => { const projectName = vault.description?.["project-metadata"].name; + const isPrivateAudit = vault.description?.["project-metadata"].isPrivateAudit; + + // Don't show the private audit, unless the project is already selected + if (isPrivateAudit && submissionData?.project?.projectId !== vault.id) return undefined; + if (projectName?.toLowerCase().includes(userInput.toLowerCase()) && !vault.liquidityPool && vault.registered) { return ( diff --git a/packages/web/src/pages/Submissions/SubmissionFormPage/encrypt.ts b/packages/web/src/pages/Submissions/SubmissionFormPage/encrypt.ts index 8de9803f6..c98fb8836 100644 --- a/packages/web/src/pages/Submissions/SubmissionFormPage/encrypt.ts +++ b/packages/web/src/pages/Submissions/SubmissionFormPage/encrypt.ts @@ -1,4 +1,5 @@ import { Key, MaybeArray, createMessage, encrypt, generateSessionKey, readKey } from "openpgp"; +import { getHatsPublicKey } from "./submissionsService.api"; const IpfsHash = require("ipfs-only-hash"); @@ -38,6 +39,24 @@ export async function encryptWithKeys(publicKeyOrKeys: string | string[], dataTo return { encryptedData, sessionKey }; } +export async function encryptWithHatsKey(dataToEncrypt: string): Promise { + try { + const hatsPublicKeyString = await getHatsPublicKey(); + if (!hatsPublicKeyString) throw new Error("Hats public key not found on server"); + + const hatsPublicKey = await readKey({ armoredKey: hatsPublicKeyString }); + const encryptedData = await encrypt({ + message: await createMessage({ text: dataToEncrypt }), + encryptionKeys: hatsPublicKey, + }); + + return encryptedData as string; + } catch (error) { + console.log(error); + throw error; + } +} + export async function calcCid(content) { return await IpfsHash.of(content); } diff --git a/packages/web/src/pages/Submissions/SubmissionFormPage/submissionsService.api.ts b/packages/web/src/pages/Submissions/SubmissionFormPage/submissionsService.api.ts index 87579c9d3..f15fdca6a 100644 --- a/packages/web/src/pages/Submissions/SubmissionFormPage/submissionsService.api.ts +++ b/packages/web/src/pages/Submissions/SubmissionFormPage/submissionsService.api.ts @@ -59,3 +59,15 @@ export async function verifyAuditWizardSignature(auditWizardSubmission: IAuditWi return false; } } + +/** + * Gets the Hats Backend public key + */ +export async function getHatsPublicKey(): Promise { + try { + const res = await axiosClient.get(`${BASE_SERVICE_URL}/pgp/public-key`); + return res.data.publicKey; + } catch (error) { + return undefined; + } +} diff --git a/packages/web/src/pages/Submissions/SubmissionFormPage/types.ts b/packages/web/src/pages/Submissions/SubmissionFormPage/types.ts index 05a0855b2..31e2f2a9c 100644 --- a/packages/web/src/pages/Submissions/SubmissionFormPage/types.ts +++ b/packages/web/src/pages/Submissions/SubmissionFormPage/types.ts @@ -26,7 +26,7 @@ export interface ISubmissionContactData { export interface ISubmissionsDescriptionsData { verified: boolean; submission: string; // Submission object ({encrypted: string, decrypted: string}) - submissionMessage: string; + submissionMessage: string; // It's only for showing on the frontend final step descriptions: { title: string; description: string; diff --git a/packages/web/src/pages/VaultEditor/VaultEditorFormPage/SetupSteps/ScopeDetailsForm/ContractsCoveredList/ContractCoveredForm/ContractCoveredForm.tsx b/packages/web/src/pages/VaultEditor/VaultEditorFormPage/SetupSteps/ScopeDetailsForm/ContractsCoveredList/ContractCoveredForm/ContractCoveredForm.tsx index adaa589d0..dd47c1d88 100644 --- a/packages/web/src/pages/VaultEditor/VaultEditorFormPage/SetupSteps/ScopeDetailsForm/ContractsCoveredList/ContractCoveredForm/ContractCoveredForm.tsx +++ b/packages/web/src/pages/VaultEditor/VaultEditorFormPage/SetupSteps/ScopeDetailsForm/ContractsCoveredList/ContractCoveredForm/ContractCoveredForm.tsx @@ -23,7 +23,7 @@ export default function ContractCoveredForm({ index, remove, contractsCount }: C fields: deployments, append: appendDeployment, remove: removeDeployment, - } = useFieldArray({ control, name: `contracts-covered.${index}.deploymentInfo` }); + } = useFieldArray({ control, name: `contracts-covered.${index}.deploymentInfo` }); const { allFormDisabled } = useContext(VaultEditorFormContext); diff --git a/packages/web/src/pages/VaultEditor/VaultEditorFormPage/SetupSteps/ScopeDetailsForm/ContractsCoveredList/ContractsCoveredList.tsx b/packages/web/src/pages/VaultEditor/VaultEditorFormPage/SetupSteps/ScopeDetailsForm/ContractsCoveredList/ContractsCoveredList.tsx index c7c4b3f56..4387648f7 100644 --- a/packages/web/src/pages/VaultEditor/VaultEditorFormPage/SetupSteps/ScopeDetailsForm/ContractsCoveredList/ContractsCoveredList.tsx +++ b/packages/web/src/pages/VaultEditor/VaultEditorFormPage/SetupSteps/ScopeDetailsForm/ContractsCoveredList/ContractsCoveredList.tsx @@ -26,7 +26,7 @@ export function ContractsCoveredList() { control, formState: { errors }, } = useEnhancedFormContext(); - const { fields: contracts, append, remove } = useFieldArray({ control, name: "contracts-covered" }); + const { fields: contracts, append, remove } = useFieldArray({ control, name: "contracts-covered" }); const reposInfo = useWatch({ control, name: "scope.reposInformation", defaultValue: [] }); const severitiesOptions = useWatch({ control, name: "severitiesOptions" }); @@ -52,6 +52,8 @@ export function ContractsCoveredList() { const contractsToCreate = await getContractsInfoFromRepos(reposInfo); append( contractsToCreate.map((contract) => ({ + name: "", + severities: [], link: "", address: contract.path, linesOfCode: contract.lines, diff --git a/packages/web/src/pages/VaultEditor/VaultEditorFormPage/SetupSteps/ScopeDetailsForm/ScopeDetailsForm.tsx b/packages/web/src/pages/VaultEditor/VaultEditorFormPage/SetupSteps/ScopeDetailsForm/ScopeDetailsForm.tsx index c43d17e4d..1e42db739 100644 --- a/packages/web/src/pages/VaultEditor/VaultEditorFormPage/SetupSteps/ScopeDetailsForm/ScopeDetailsForm.tsx +++ b/packages/web/src/pages/VaultEditor/VaultEditorFormPage/SetupSteps/ScopeDetailsForm/ScopeDetailsForm.tsx @@ -1,5 +1,5 @@ import { CODE_LANGUAGES, DEFAULT_OUT_OF_SCOPE, DEFAULT_TOOLING_STEPS, IEditedVaultDescription } from "@hats-finance/shared"; -import { Button, FormInput, FormMDEditor, FormRadioInput, Pill } from "components"; +import { Alert, Button, FormInput, FormMDEditor, FormRadioInput, Pill } from "components"; import { getCustomIsDirty, useEnhancedFormContext } from "hooks/form"; import useConfirm from "hooks/useConfirm"; import { useContext, useMemo } from "react"; @@ -19,6 +19,7 @@ export const ScopeDetailsForm = () => { const { control, register, setValue, getValues, watch } = useEnhancedFormContext(); const vaultType = useWatch({ control, name: "project-metadata.type" }); + const isPrivateAudit = useWatch({ control, name: "project-metadata.isPrivateAudit" }); const isAudit = vaultType === "audit"; const handleClickOnCodeLang = (codeLang: string, checked: boolean) => { @@ -67,8 +68,15 @@ export const ScopeDetailsForm = () => { return ( + {/* Private audits alert */} + {isAdvancedMode && isPrivateAudit && ( + + {t("scopePrivateAuditsWarning")} + + )} + {/* Project Outline */} - {isAdvancedMode && ( + {isAdvancedMode && !isPrivateAudit && ( <>
{t("vaultEditorScopeExplanation")}

{t("offerDescriptionHowTheProtocolWorks")}

@@ -90,7 +98,7 @@ export const ScopeDetailsForm = () => { )} {/* Project Coding languages */} - {isAdvancedMode && ( + {isAdvancedMode && !isPrivateAudit && ( <>

{t("VaultEditor.selectCodeLanguages")}

@@ -118,10 +126,14 @@ export const ScopeDetailsForm = () => { )} {/* Repos and documentation */} -

{t("VaultEditor.reposAndDocumentation")}

- + {isAdvancedMode && !isPrivateAudit && ( + <> +

{t("VaultEditor.reposAndDocumentation")}

+ + + )} - {isAdvancedMode && ( + {isAdvancedMode && !isPrivateAudit && ( <>

{t("VaultEditor.linkToProtocolDocs")}

{ ) : null} {/* Out of scope */} - {isAdvancedMode && ( + {isAdvancedMode && !isPrivateAudit && ( <>

{t("VaultEditor.outOfScope")} @@ -175,7 +187,7 @@ export const ScopeDetailsForm = () => { )} {/* Steps to run project */} - {isAdvancedMode && ( + {isAdvancedMode && !isPrivateAudit && ( <>

{t("VaultEditor.stepsToRunProject")} diff --git a/packages/web/src/pages/VaultEditor/VaultEditorFormPage/SetupSteps/ScopeDetailsForm/ScopeReposInformation/ScopeReposInformation.tsx b/packages/web/src/pages/VaultEditor/VaultEditorFormPage/SetupSteps/ScopeDetailsForm/ScopeReposInformation/ScopeReposInformation.tsx index 63d37f592..6d90db7d9 100644 --- a/packages/web/src/pages/VaultEditor/VaultEditorFormPage/SetupSteps/ScopeDetailsForm/ScopeReposInformation/ScopeReposInformation.tsx +++ b/packages/web/src/pages/VaultEditor/VaultEditorFormPage/SetupSteps/ScopeDetailsForm/ScopeReposInformation/ScopeReposInformation.tsx @@ -29,6 +29,7 @@ export const ScopeReposInformation = () => { ); const vaultType = useWatch({ control, name: "project-metadata.type" }); + const isPrivateAudit = useWatch({ control, name: "project-metadata.isPrivateAudit" }); // Only one repo can be the main repo useOnChange(repos, (newRepos, prevRepos) => { @@ -66,7 +67,13 @@ export const ScopeReposInformation = () => {

diff --git a/packages/web/src/pages/VaultEditor/VaultEditorFormPage/SetupSteps/VaultDetailsForm/VaultDetailsForm.tsx b/packages/web/src/pages/VaultEditor/VaultEditorFormPage/SetupSteps/VaultDetailsForm/VaultDetailsForm.tsx index b5164b1ba..9132bf24d 100644 --- a/packages/web/src/pages/VaultEditor/VaultEditorFormPage/SetupSteps/VaultDetailsForm/VaultDetailsForm.tsx +++ b/packages/web/src/pages/VaultEditor/VaultEditorFormPage/SetupSteps/VaultDetailsForm/VaultDetailsForm.tsx @@ -15,6 +15,7 @@ import { useTranslation } from "react-i18next"; import { VaultEditorFormContext } from "../../store"; import { VaultAssetsList } from "../shared/VaultAssetsList/VaultAssetsList"; import { VaultEmailsForm } from "../shared/VaultEmailsList/VaultEmailsList"; +import { WhitelistedAddressesList } from "../shared/WhitelistedAddressesList/WhitelistedAddressesList"; import { StyledVaultDetails } from "./styles"; export function VaultDetailsForm() { @@ -25,6 +26,7 @@ export function VaultDetailsForm() { const showDateInputs = useWatch({ control, name: "includesStartAndEndTime" }); const vaultType = useWatch({ control, name: "project-metadata.type" }); + const isPrivateAudit = useWatch({ control, name: "project-metadata.isPrivateAudit" }); const vaultTypes = [ { label: t("bugBountyProgram"), value: "normal" }, @@ -90,8 +92,9 @@ export function VaultDetailsForm() { {...register("project-metadata.name")} label={t("VaultEditor.vault-details.name")} colorable - disabled={allFormDisabled} + disabled={(isEditingExistingVault && vaultType === "audit") || allFormDisabled} placeholder={t("VaultEditor.vault-details.name-placeholder")} + flexExpand /> )} /> + {(vaultType === "audit" && isAdvancedMode) || isPrivateAudit ? ( + + ) : null}
@@ -142,6 +154,14 @@ export function VaultDetailsForm() { />
+ {isPrivateAudit && ( + <> +

{t("VaultEditor.vault-details.whitelistedAddreses")}

+

{t("VaultEditor.vault-details.whitelistedAddresesExplanation")}

+ + + )} + {!isEditingExistingVault && ( <>

{t("communicationChannel")}

diff --git a/packages/web/src/pages/VaultEditor/VaultEditorFormPage/SetupSteps/shared/VaultAssetsList/VaultAssetsList.tsx b/packages/web/src/pages/VaultEditor/VaultEditorFormPage/SetupSteps/shared/VaultAssetsList/VaultAssetsList.tsx index 501310d11..7f2779269 100644 --- a/packages/web/src/pages/VaultEditor/VaultEditorFormPage/SetupSteps/shared/VaultAssetsList/VaultAssetsList.tsx +++ b/packages/web/src/pages/VaultEditor/VaultEditorFormPage/SetupSteps/shared/VaultAssetsList/VaultAssetsList.tsx @@ -1,11 +1,11 @@ -import { useFieldArray } from "react-hook-form"; import { useEnhancedFormContext } from "hooks/form/useEnhancedFormContext"; +import { useFieldArray } from "react-hook-form"; import { IEditedVaultDescription } from "types"; import { VaultAssetForm } from "./VaultAssetForm/VaultAssetForm"; export const VaultAssetsList = () => { const { control } = useEnhancedFormContext(); - const { fields: assets, append, remove } = useFieldArray({ control, name: "assets" }); + const { fields: assets, append, remove } = useFieldArray({ control, name: "assets" }); return ( <> diff --git a/packages/web/src/pages/VaultEditor/VaultEditorFormPage/SetupSteps/shared/WhitelistedAddressesList/WhitelistedAddressForm/WhitelistedAddressForm.tsx b/packages/web/src/pages/VaultEditor/VaultEditorFormPage/SetupSteps/shared/WhitelistedAddressesList/WhitelistedAddressForm/WhitelistedAddressForm.tsx new file mode 100644 index 000000000..cb4705dbe --- /dev/null +++ b/packages/web/src/pages/VaultEditor/VaultEditorFormPage/SetupSteps/shared/WhitelistedAddressesList/WhitelistedAddressForm/WhitelistedAddressForm.tsx @@ -0,0 +1,39 @@ +import DeleteIcon from "@mui/icons-material/DeleteOutlineOutlined"; +import { Button, FormInput } from "components"; +import { useEnhancedFormContext } from "hooks/form"; +import { VaultEditorFormContext } from "pages/VaultEditor/VaultEditorFormPage/store"; +import { useContext } from "react"; +import { useTranslation } from "react-i18next"; +import { IEditedVaultDescription } from "types"; +import { StyledWhitelistedAddressForm } from "./styles"; + +type WhitelistedAddressFormProps = { + index: number; + remove: (index: number) => void; +}; + +export const WhitelistedAddressForm = ({ index, remove }: WhitelistedAddressFormProps) => { + const { t } = useTranslation(); + + const { register } = useEnhancedFormContext(); + const { allFormDisabled } = useContext(VaultEditorFormContext); + + return ( + <> + + + + + + ); +}; diff --git a/packages/web/src/pages/VaultEditor/VaultEditorFormPage/SetupSteps/shared/WhitelistedAddressesList/WhitelistedAddressForm/styles.ts b/packages/web/src/pages/VaultEditor/VaultEditorFormPage/SetupSteps/shared/WhitelistedAddressesList/WhitelistedAddressForm/styles.ts new file mode 100644 index 000000000..de6281022 --- /dev/null +++ b/packages/web/src/pages/VaultEditor/VaultEditorFormPage/SetupSteps/shared/WhitelistedAddressesList/WhitelistedAddressForm/styles.ts @@ -0,0 +1,21 @@ +import styled from "styled-components"; +import { getSpacing } from "styles"; + +export const StyledWhitelistedAddressForm = styled.div` + display: flex; + align-items: baseline; + + & > :nth-child(1) { + width: calc(100% - 160px); + } + + & > :nth-child(2) { + width: 160px; + display: flex; + justify-content: center; + } + + &:not(:last-of-type) { + margin-bottom: ${getSpacing(2)}; + } +`; diff --git a/packages/web/src/pages/VaultEditor/VaultEditorFormPage/SetupSteps/shared/WhitelistedAddressesList/WhitelistedAddressesList.tsx b/packages/web/src/pages/VaultEditor/VaultEditorFormPage/SetupSteps/shared/WhitelistedAddressesList/WhitelistedAddressesList.tsx new file mode 100644 index 000000000..8891f9c37 --- /dev/null +++ b/packages/web/src/pages/VaultEditor/VaultEditorFormPage/SetupSteps/shared/WhitelistedAddressesList/WhitelistedAddressesList.tsx @@ -0,0 +1,48 @@ +import AddIcon from "@mui/icons-material/Add"; +import { Button } from "components"; +import { useEnhancedFormContext } from "hooks/form/useEnhancedFormContext"; +import { useContext } from "react"; +import { useFieldArray } from "react-hook-form"; +import { useTranslation } from "react-i18next"; +import { IEditedVaultDescription } from "types"; +import { getPath } from "utils/objects.utils"; +import { VaultEditorFormContext } from "../../../store"; +import { WhitelistedAddressForm } from "./WhitelistedAddressForm/WhitelistedAddressForm"; +import { StyledWhitelistedAddressesList } from "./styles"; + +export const WhitelistedAddressesList = () => { + const { t } = useTranslation(); + const { allFormDisabled } = useContext(VaultEditorFormContext); + + const { + control, + formState: { errors }, + } = useEnhancedFormContext(); + + const { + fields: whitelistedAddresses, + append: appendWhitelistedAddress, + remove: removeWhitelistedAddress, + } = useFieldArray({ control, name: `project-metadata.whitelist` }); + + return ( + +
+ {whitelistedAddresses.map((whitelistedAddress, emailIndex) => { + return ; + })} + + {getPath(errors, "project-metadata.whitelist") && ( +

{getPath(errors, "project-metadata.whitelist")?.message}

+ )} + + {!allFormDisabled && ( + + )} +
+
+ ); +}; diff --git a/packages/web/src/pages/VaultEditor/VaultEditorFormPage/SetupSteps/shared/WhitelistedAddressesList/styles.ts b/packages/web/src/pages/VaultEditor/VaultEditorFormPage/SetupSteps/shared/WhitelistedAddressesList/styles.ts new file mode 100644 index 000000000..b2bfb3cad --- /dev/null +++ b/packages/web/src/pages/VaultEditor/VaultEditorFormPage/SetupSteps/shared/WhitelistedAddressesList/styles.ts @@ -0,0 +1,9 @@ +import styled from "styled-components"; + +export const StyledWhitelistedAddressesList = styled.div` + width: 100%; + + .whitelistedAddresses { + width: 100%; + } +`; diff --git a/packages/web/src/pages/VaultEditor/VaultEditorFormPage/VaultEditorFormPage.tsx b/packages/web/src/pages/VaultEditor/VaultEditorFormPage/VaultEditorFormPage.tsx index 17421f01f..888782dbc 100644 --- a/packages/web/src/pages/VaultEditor/VaultEditorFormPage/VaultEditorFormPage.tsx +++ b/packages/web/src/pages/VaultEditor/VaultEditorFormPage/VaultEditorFormPage.tsx @@ -143,47 +143,6 @@ const VaultEditorFormPage = () => { checkGovMember(); }, [address, chain]); - const createOrSaveEditSession = async (isCreation = false, withIpfsHash = false) => { - try { - // If vault is already created or is isNonEditableStatus, edition is blocked - if (isNonEditableStatus) return; - if (allFormDisabled) return; - if (isSomeoneCreatingTheVault) return; - if (!isCreation && !formState.isDirty) return; - if (isCreation) setLoadingEditSession(true); - if (!isCreation) setSavingEditSession(true); - - let sessionIdOrSessionResponse: string | IEditedSessionResponse; - - if (isCreation) { - sessionIdOrSessionResponse = await VaultEditorService.upsertEditSession( - undefined, - undefined, - withIpfsHash ? editSessionId : undefined - ); - } else { - const data: IEditedVaultDescription = getValues(); - sessionIdOrSessionResponse = await VaultEditorService.upsertEditSession(data, editSessionId, undefined); - } - - if (typeof sessionIdOrSessionResponse === "string") { - navigate(`${RoutePaths.vault_editor}/${sessionIdOrSessionResponse}`, { replace: true }); - } else { - refreshEditSessionData(sessionIdOrSessionResponse); - } - - setSavingEditSession(false); - setLoadingEditSession(false); - } catch (error) { - setSavingEditSession(false); - setLoadingEditSession(false); - - if (!editSessionId) return; - const editSessionResponse = await VaultEditorService.getEditSessionData(editSessionId); - refreshEditSessionData(editSessionResponse); - } - }; - async function loadEditSessionData(editSessionId: string) { if (isVaultCreated) return; // If vault is already created, creation is blocked @@ -217,24 +176,80 @@ const VaultEditorFormPage = () => { } } - const refreshEditSessionData = async (newEditSession: IEditedSessionResponse, withReset = true) => { - const wasSubmittedToCreation = (newEditSession.submittedToCreation ?? false) && !newEditSession.vaultAddress; - - setEditSessionSubmittedCreation(wasSubmittedToCreation); - setDescriptionHash(newEditSession.descriptionHash); - setLastModifedOn(newEditSession.updatedAt); - setEditingExistingVaultStatus(newEditSession.vaultEditionStatus); - setIsSomeoneCreatingTheVault( - (!wasSubmittedToCreation && - newEditSession.lastCreationOnChainRequest && - moment().diff(moment(newEditSession.lastCreationOnChainRequest), "minute") < 5) ?? - false - ); + const refreshEditSessionData = useCallback( + async (newEditSession: IEditedSessionResponse, withReset = true) => { + const wasSubmittedToCreation = (newEditSession.submittedToCreation ?? false) && !newEditSession.vaultAddress; + + setEditSessionSubmittedCreation(wasSubmittedToCreation); + setDescriptionHash(newEditSession.descriptionHash); + setLastModifedOn(newEditSession.updatedAt); + setEditingExistingVaultStatus(newEditSession.vaultEditionStatus); + setIsSomeoneCreatingTheVault( + (!wasSubmittedToCreation && + newEditSession.lastCreationOnChainRequest && + moment().diff(moment(newEditSession.lastCreationOnChainRequest), "minute") < 5) ?? + false + ); - if (withReset) handleReset(newEditSession.editedDescription, { keepErrors: true }); - }; + if (withReset) handleReset(newEditSession.editedDescription, { keepErrors: true }); + }, + [handleReset] + ); + + const createOrSaveEditSession = useCallback( + async (isCreation = false, withIpfsHash = false) => { + try { + // If vault is already created or is isNonEditableStatus, edition is blocked + if (isNonEditableStatus) return; + if (allFormDisabled) return; + if (isSomeoneCreatingTheVault) return; + if (!isCreation && !formState.isDirty) return; + if (isCreation) setLoadingEditSession(true); + if (!isCreation) setSavingEditSession(true); + + let sessionIdOrSessionResponse: string | IEditedSessionResponse; + + if (isCreation) { + sessionIdOrSessionResponse = await VaultEditorService.upsertEditSession( + undefined, + undefined, + withIpfsHash ? editSessionId : undefined + ); + } else { + const data: IEditedVaultDescription = getValues(); + sessionIdOrSessionResponse = await VaultEditorService.upsertEditSession(data, editSessionId, undefined); + } - const createVaultOnChain = async () => { + if (typeof sessionIdOrSessionResponse === "string") { + navigate(`${RoutePaths.vault_editor}/${sessionIdOrSessionResponse}`, { replace: true }); + } else { + refreshEditSessionData(sessionIdOrSessionResponse); + } + + setSavingEditSession(false); + setLoadingEditSession(false); + } catch (error) { + setSavingEditSession(false); + setLoadingEditSession(false); + + if (!editSessionId) return; + const editSessionResponse = await VaultEditorService.getEditSessionData(editSessionId); + refreshEditSessionData(editSessionResponse); + } + }, + [ + allFormDisabled, + editSessionId, + formState.isDirty, + getValues, + isNonEditableStatus, + isSomeoneCreatingTheVault, + navigate, + refreshEditSessionData, + ] + ); + + const createVaultOnChain = useCallback(async () => { if (allFormDisabled) return; try { @@ -294,7 +309,7 @@ const VaultEditorFormPage = () => { console.error(error); setCreatingVault(false); } - }; + }, [address, allFormDisabled, confirm, descriptionHash, editSessionId, getValues, navigate, refreshEditSessionData, t]); const sendEditionToGovApproval = async () => { if (allFormDisabled) return; diff --git a/packages/web/src/pages/VaultEditor/VaultEditorFormPage/formSchema.ts b/packages/web/src/pages/VaultEditor/VaultEditorFormPage/formSchema.ts index 9793831a9..145731456 100644 --- a/packages/web/src/pages/VaultEditor/VaultEditorFormPage/formSchema.ts +++ b/packages/web/src/pages/VaultEditor/VaultEditorFormPage/formSchema.ts @@ -23,6 +23,17 @@ export const getEditedDescriptionYupSchema = (intl: TFunction) => website: Yup.string().test(getTestUrl(intl)).required(intl("required")), name: Yup.string().required(intl("required")), type: Yup.string().required(intl("required")).typeError(intl("required")), + isPrivateAudit: Yup.boolean(), + whitelist: Yup.array() + .of( + Yup.object({ + address: Yup.string().test(getTestWalletAddress(intl)).required(intl("required")), + }) + ) + .when("isPrivateAudit", (isPrivateAudit: boolean, schema: any) => { + if (!isPrivateAudit) return schema; + return schema.required(intl("required")).min(1, intl("required")); + }), oneLiner: Yup.string() .required(intl("required")) .typeError(intl("required")) @@ -61,8 +72,12 @@ export const getEditedDescriptionYupSchema = (intl: TFunction) => scope: Yup.object({ reposInformation: Yup.array().of( Yup.object({ - url: Yup.string().test(getTestGithubRepoUrl(intl)).required(intl("required")), - commitHash: Yup.string().test(getTestGitCommitHash(intl)).required(intl("required")), + url: Yup.string() + .test(getTestGithubRepoUrl(intl)) + .test("required", intl("required"), (val, ctx: any) => !!ctx.from[2].value["project-metadata"]?.isPrivateAudit), + commitHash: Yup.string() + .test(getTestGitCommitHash(intl)) + .test("required", intl("required"), (val, ctx: any) => !!ctx.from[2].value["project-metadata"]?.isPrivateAudit), isMain: Yup.boolean(), }) ), diff --git a/packages/web/src/utils/contractsCovered.utils.ts b/packages/web/src/utils/contractsCovered.utils.ts index 8b9088d94..424853ad1 100644 --- a/packages/web/src/utils/contractsCovered.utils.ts +++ b/packages/web/src/utils/contractsCovered.utils.ts @@ -20,7 +20,6 @@ export async function getContractsInfoFromRepos(repos: IVaultRepoInformation[]): sessionStorage.getItem(`repoContracts-${repos.map((repo) => repo.commitHash).join("-")}`) ?? "null" ); - console.log(dataInStorage); if (dataInStorage) return dataInStorage; for (const repo of repos) { diff --git a/packages/web/src/utils/tokens.utils.ts b/packages/web/src/utils/tokens.utils.ts index dfbec046c..c2f37e4e3 100644 --- a/packages/web/src/utils/tokens.utils.ts +++ b/packages/web/src/utils/tokens.utils.ts @@ -1,10 +1,12 @@ -import axios, { AxiosResponse } from "axios"; -import { fetchToken } from "wagmi/actions"; import { TokenPriceResponse } from "@hats-finance/shared"; -import { appChains } from "settings"; +import axios, { AxiosResponse } from "axios"; +import { GET_PRICES_BALANCER, IBalancerGetPricesResponse } from "graphql/balancer"; import { GET_PRICES_UNISWAP, IUniswapGetPricesResponse } from "graphql/uniswap"; +import { appChains } from "settings"; +import { fetchToken } from "wagmi/actions"; const COIN_GECKO_ENDPOINT = "https://api.coingecko.com/api/v3/simple/token_price"; +const BALANCER_SUBGRAPH_ENDPOINT = "https://api-v3.balancer.fi/graphql"; export const getTokenInfo = async ( address: string, @@ -103,3 +105,23 @@ export const getUniswapTokenPrices = async (tokens: { address: string; chainId: throw new Error(`Error getting prices: ${err}`); } }; + +export const getBalancerTokenPrices = async (tokens: { address: string; chainId: number }[]): Promise => { + try { + const balancerResponse: AxiosResponse = await axios.post(BALANCER_SUBGRAPH_ENDPOINT, { + query: GET_PRICES_BALANCER, + }); + + return tokens.reduce((acc, token) => { + const tokenPrice = balancerResponse.data.data.tokenGetCurrentPrices.find( + (t) => t.address.toLowerCase() === token.address.toLowerCase() + ); + + if (tokenPrice) acc[token.address] = { usd: +tokenPrice.price }; + return { ...acc }; + }, {} as TokenPriceResponse); + } catch (err) { + console.error(err); + throw new Error(`Error getting prices: ${err}`); + } +}; diff --git a/yarn.lock b/yarn.lock index 0bc88edb7..1ce87f015 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5179,6 +5179,11 @@ assertion-error@^1.1.0: resolved "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz" integrity sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw== +assignment@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/assignment/-/assignment-2.0.0.tgz#ffd17b21bf5d6b22e777b989681a815456a3dd3e" + integrity sha512-naMULXjtgCs9SVUEtyvJNt68aF18em7/W+dhbR59kbz9cXWPEvUkCun2tqlgqRPSqZaKPpqLc5ZnwL8jVmJRvw== + ast-types-flow@^0.0.7: version "0.0.7" resolved "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz" @@ -8671,9 +8676,9 @@ get-caller-file@^2.0.1, get-caller-file@^2.0.5: integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== get-func-name@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz" - integrity sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig== + version "2.0.2" + resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.2.tgz#0d7cf20cd13fda808669ffa88f4ffc7a3943fc41" + integrity sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ== get-intrinsic@^1.0.2, get-intrinsic@^1.1.1, get-intrinsic@^1.1.3: version "1.2.0" @@ -8884,10 +8889,10 @@ graphql-tag@^2.12.6: dependencies: tslib "^2.1.0" -graphql@^16.5.0, graphql@^16.6.0: - version "16.6.0" - resolved "https://registry.npmjs.org/graphql/-/graphql-16.6.0.tgz" - integrity sha512-KPIBPDlW7NxrbT/eh4qPXz5FiFdL5UbaA0XUNz2Rp3Z3hqBSkbj0GVjwFDztsWVauZUWsbKHgMg++sk8UX0bkw== +graphql@^16.6.0, graphql@^16.8.1: + version "16.8.1" + resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.8.1.tgz#1930a965bef1170603702acdb68aedd3f3cf6f07" + integrity sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw== gzip-size@^6.0.0: version "6.0.0" @@ -13778,10 +13783,10 @@ react-helmet@^6.1.0: react-fast-compare "^3.1.1" react-side-effect "^2.1.0" -react-hook-form@^7.42.1: - version "7.43.0" - resolved "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.43.0.tgz" - integrity sha512-/rVEz7T0gLdSFwPqutJ1kn2e0sQNyb9ci/hmwEYr2YG0KF/LSuRLvNrf9QWJM+gj88CjDpDW5Bh/1AD7B2+z9Q== +react-hook-form@^7.46.2: + version "7.46.2" + resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.46.2.tgz#051e3cb2a73f3e86de739f2198c6042902158c43" + integrity sha512-x1DWmHQchV7x2Rq9l99M/cQHC8JGchAnw9Z0uTz5KrPa0bTl/Inm1NR7ceOARfIrkNuQNAhuSuZPYa6k7QYn3Q== react-https-redirect@^1.1.0: version "1.1.0" @@ -14578,6 +14583,13 @@ safe-stable-stringify@^2.1.0: resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== +sanitize-markdown@^2.6.7: + version "2.6.7" + resolved "https://registry.yarnpkg.com/sanitize-markdown/-/sanitize-markdown-2.6.7.tgz#335d7627fe15339cf5be5a0ae78590c097007e33" + integrity sha512-w0exIpQ6Zk/q4yUZNgYcxz5sO1o+y3zYeJlHXp+AGKUPOy9wUoKUpu6/+qVlW6oGMvQ72N/6Gx8q/tZG/CKxSA== + dependencies: + assignment "2.0.0" + sanitize.css@*: version "13.0.0" resolved "https://registry.npmjs.org/sanitize.css/-/sanitize.css-13.0.0.tgz"