diff --git a/packages/shared/src/types/bonusSubmissions.ts b/packages/shared/src/types/bonusSubmissions.ts
new file mode 100644
index 000000000..629dd5066
--- /dev/null
+++ b/packages/shared/src/types/bonusSubmissions.ts
@@ -0,0 +1,7 @@
+export interface IClaimedIssue {
+ vaultAddress: string;
+ issueNumber: string;
+ claimedBy: string;
+ claimedAt: Date;
+ expiresAt: Date;
+}
diff --git a/packages/shared/src/types/editor.ts b/packages/shared/src/types/editor.ts
index 052ee0a4e..ce2d0bdc1 100644
--- a/packages/shared/src/types/editor.ts
+++ b/packages/shared/src/types/editor.ts
@@ -80,6 +80,7 @@ export interface IBaseEditedVaultDescription {
isPrivateAudit?: boolean;
isContinuousAudit?: boolean;
requireMessageSignature?: boolean;
+ bonusPointsEnabled?: boolean;
messageToSign?: string;
whitelist: { address: string }[];
endtime?: number;
diff --git a/packages/shared/src/types/index.ts b/packages/shared/src/types/index.ts
index 1fe858e3f..07f061ed5 100644
--- a/packages/shared/src/types/index.ts
+++ b/packages/shared/src/types/index.ts
@@ -4,3 +4,4 @@ export * from "./payout";
export * from "./safe";
export * from "./submissions";
export * from "./profile";
+export * from "./bonusSubmissions";
diff --git a/packages/shared/src/types/payout.ts b/packages/shared/src/types/payout.ts
index ac9407c7a..8a9335b84 100644
--- a/packages/shared/src/types/payout.ts
+++ b/packages/shared/src/types/payout.ts
@@ -49,6 +49,10 @@ export type GithubIssue = {
createdBy: number;
labels: string[];
validLabels: string[];
+ bonusPointsLabels: {
+ needsFix: boolean;
+ needsTest: boolean;
+ };
createdAt: string;
body: string;
txHash?: string;
diff --git a/packages/shared/src/types/types.ts b/packages/shared/src/types/types.ts
index f216c5355..73b437641 100644
--- a/packages/shared/src/types/types.ts
+++ b/packages/shared/src/types/types.ts
@@ -141,6 +141,7 @@ interface IBaseVaultDescription {
isPrivateAudit?: boolean;
isContinuousAudit?: boolean;
requireMessageSignature?: boolean;
+ bonusPointsEnabled?: boolean;
messageToSign?: string;
whitelist: { address: string }[];
endtime?: number;
diff --git a/packages/web/src/components/Button/styles.ts b/packages/web/src/components/Button/styles.ts
index b6b109947..fb2e4a3f0 100644
--- a/packages/web/src/components/Button/styles.ts
+++ b/packages/web/src/components/Button/styles.ts
@@ -36,6 +36,8 @@ const getVariableByTextColor = (textColor: ButtonProps["textColor"], styleType:
return "--primary";
case "error":
return "--error-red";
+ case "white":
+ return "--white";
default:
if (styleType === "invisible") return "--secondary";
return "--white";
diff --git a/packages/web/src/components/FormControls/FormSupportFilesInput/FormSupportFilesInput.tsx b/packages/web/src/components/FormControls/FormSupportFilesInput/FormSupportFilesInput.tsx
index 0edb63e18..11038b700 100644
--- a/packages/web/src/components/FormControls/FormSupportFilesInput/FormSupportFilesInput.tsx
+++ b/packages/web/src/components/FormControls/FormSupportFilesInput/FormSupportFilesInput.tsx
@@ -20,10 +20,22 @@ type FormSupportFilesInputProps = {
name: string;
onChange: (data: ISavedFile[]) => void;
error?: { message?: string; type: string };
+ uploadTo?: "db" | "ipfs";
+ noFilesAttachedInfo?: boolean;
};
export const FormSupportFilesInputComponent = (
- { colorable = false, isDirty = false, name, onChange, label, error, value }: FormSupportFilesInputProps,
+ {
+ colorable = false,
+ isDirty = false,
+ name,
+ onChange,
+ label,
+ error,
+ value,
+ uploadTo = "db",
+ noFilesAttachedInfo,
+ }: FormSupportFilesInputProps,
ref
) => {
const { t } = useTranslation();
@@ -50,7 +62,7 @@ export const FormSupportFilesInputComponent = (
return alert(t("invalid-file-type"));
}
- filesToUploadPromises.push(FilesService.uploadFileToDB(file));
+ filesToUploadPromises.push(FilesService.uploadFileToDB(file, uploadTo === "ipfs"));
}
const uploadedFiles = await Promise.all(filesToUploadPromises);
@@ -76,17 +88,19 @@ export const FormSupportFilesInputComponent = (
{isUploadingFiles ? `${t("uploadingFiles")}...` : label ?? ""}
-
-
{t("filesAttached")}:
-
- {value?.map((file, idx) => (
- -
- handleRemoveFile(idx)} />
-
{file.name}
-
- ))}
-
-
+ {!noFilesAttachedInfo && (
+
+
{t("filesAttached")}:
+
+ {value?.map((file, idx) => (
+ -
+ handleRemoveFile(idx)} />
+
{file.name}
+
+ ))}
+
+
+ )}
{error && {error.message}}
diff --git a/packages/web/src/components/FormControls/FormSupportFilesInput/supportedExtensions.ts b/packages/web/src/components/FormControls/FormSupportFilesInput/supportedExtensions.ts
index 6b3206042..063113c31 100644
--- a/packages/web/src/components/FormControls/FormSupportFilesInput/supportedExtensions.ts
+++ b/packages/web/src/components/FormControls/FormSupportFilesInput/supportedExtensions.ts
@@ -1 +1 @@
-export const supportedExtensions = ["txt", "sol", "ts", "js"];
+export const supportedExtensions = ["txt", "sol", "ts", "js", "md", "json"];
diff --git a/packages/web/src/components/VaultCard/VaultCard.tsx b/packages/web/src/components/VaultCard/VaultCard.tsx
index 926668d38..1e4e7afe7 100644
--- a/packages/web/src/components/VaultCard/VaultCard.tsx
+++ b/packages/web/src/components/VaultCard/VaultCard.tsx
@@ -159,6 +159,8 @@ export const VaultCard = ({
ONE_LINER_FALLBACK[vault.id] ??
"Nulla facilisi. Donec nec dictum eros. Cras et velit viverra, dapibus velit fringilla, bibendum mi aptent. Class aptent taciti sociosqu ad litora.";
+ const bonusPointsEnabled = vault.description?.["project-metadata"]?.bonusPointsEnabled;
+
const getAuditStatusPill = () => {
if (!vault.description) return null;
if (!vault.description["project-metadata"].endtime) return null;
@@ -470,9 +472,10 @@ export const VaultCard = ({
{!reducedStyles && (
-
{auditPayout ? t("paidAssets") : t("assetsInVault")}
-
-
+
+ {auditPayout ? t("paidAssets") : t("assetsInVault")}
+
+
@@ -487,6 +490,17 @@ export const VaultCard = ({
{t("deposits")}
)}
+ {isAudit && vault.dateStatus === "on_time" && !auditPayout && !hideSubmit && bonusPointsEnabled && (
+
+ )}
{(!isAudit || (isAudit && vault.dateStatus === "on_time" && !auditPayout)) && !hideSubmit && (
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 b052d86f3..5dd2cf8b1 100644
--- a/packages/web/src/pages/Honeypots/VaultDetailsPage/Sections/VaultRewardsSection/styles.ts
+++ b/packages/web/src/pages/Honeypots/VaultDetailsPage/Sections/VaultRewardsSection/styles.ts
@@ -67,6 +67,40 @@ export const StyledRewardsSection = styled.div<{ showIntended: boolean; isAudit:
grid-column-start: 1;
grid-column-end: 3;
}
+
+ .bonus-points-info-container {
+ border-top: 1px solid var(--primary-light);
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ gap: ${getSpacing(1)};
+ padding-top: ${getSpacing(3)};
+
+ p {
+ font-weight: 700;
+ }
+
+ .points {
+ display: flex;
+ gap: ${getSpacing(2)};
+
+ .point {
+ display: flex;
+ align-items: center;
+ gap: ${getSpacing(1)};
+
+ .secondary-text {
+ color: var(--secondary);
+ font-weight: 700;
+ }
+
+ .primary-text {
+ color: var(--primary);
+ font-weight: 700;
+ }
+ }
+ }
+ }
}
.card {
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
index 8d60f48dc..e1972df24 100644
--- a/packages/web/src/pages/Honeypots/VaultDetailsPage/Sections/VaultSubmissionsSection/PublicSubmissionCard/PublicSubmissionCard.tsx
+++ b/packages/web/src/pages/Honeypots/VaultDetailsPage/Sections/VaultSubmissionsSection/PublicSubmissionCard/PublicSubmissionCard.tsx
@@ -1,16 +1,15 @@
-import { IVault, IVulnerabilitySeverity, allowedElementsMarkdown } from "@hats.finance/shared";
+import { GithubIssue, IVault, allowedElementsMarkdown, parseSeverityName } 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 { SplitPointsActions } from "./components/SplitPointsActions";
import { StyledPublicSubmissionCard } from "./styles";
type PublicSubmissionCardProps = {
vault: IVault;
- submission: IGithubIssue;
+ submission: GithubIssue;
};
function PublicSubmissionCard({ vault, submission }: PublicSubmissionCardProps) {
@@ -18,28 +17,41 @@ function PublicSubmissionCard({ vault, submission }: PublicSubmissionCardProps)
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 ?? "")
- );
+ const showExtraInfo = submission.number !== -1;
+ const bonusPointsEnabled = vault.description?.["project-metadata"]?.bonusPointsEnabled;
return (
- setIsOpen((prev) => !prev)}>
-
-
+
+
setIsOpen((prev) => !prev)}>
+ {submission && submission?.validLabels.length > 0 && (
+
+
{t("labeledAs")}:
+ {submission.validLabels.map((label) => (
+
+ ))}
+
+ )}
+
{moment(submission.createdAt).format("Do MMM YYYY - hh:mma")}
+
+ {showExtraInfo ? Issue #{submission.number}: : ""} {submission.title}
+
-
{moment(submission.createdAt).format("Do MMM YYYY - hh:mma")}
-
{submission.issueData.issueTitle}
+
+ {showExtraInfo &&
+ bonusPointsEnabled &&
+ (submission.bonusPointsLabels.needsFix || submission.bonusPointsLabels.needsTest) && (
+
+ )}
diff --git a/packages/web/src/pages/Honeypots/VaultDetailsPage/Sections/VaultSubmissionsSection/PublicSubmissionCard/claimIssuesService.ts b/packages/web/src/pages/Honeypots/VaultDetailsPage/Sections/VaultSubmissionsSection/PublicSubmissionCard/claimIssuesService.ts
new file mode 100644
index 000000000..9c2e32d79
--- /dev/null
+++ b/packages/web/src/pages/Honeypots/VaultDetailsPage/Sections/VaultSubmissionsSection/PublicSubmissionCard/claimIssuesService.ts
@@ -0,0 +1,35 @@
+import { IClaimedIssue, IVault } from "@hats.finance/shared";
+import { AxiosError } from "axios";
+import { axiosClient } from "config/axiosClient";
+import { BASE_SERVICE_URL } from "settings";
+
+/**
+ * Claims an issue for a vault
+ */
+export async function claimIssue(vault: IVault, issueNumber: number): Promise
{
+ try {
+ const response = await axiosClient.post(`${BASE_SERVICE_URL}/submission-bonus-points/claim/${vault.id}`, {
+ issueNumber,
+ });
+ return response.data.claimedIssue;
+ } catch (error) {
+ console.log(error);
+ throw ((error as AxiosError).response?.data as any)?.error;
+ }
+}
+
+/**
+ * Gets claimed issues for a vault
+ */
+export async function getClaimedIssuesByVault(vault: IVault): Promise {
+ const response = await axiosClient.get(`${BASE_SERVICE_URL}/submission-bonus-points/claim/${vault.id}`);
+ return response.data.claimedIssues ?? [];
+}
+
+/**
+ * Gets claimed issues for a vault and a claimed by
+ */
+export async function getClaimedIssuesByVaultAndClaimedBy(vault: IVault, claimedBy: string): Promise {
+ const response = await axiosClient.get(`${BASE_SERVICE_URL}/submission-bonus-points/claim/${vault.id}/${claimedBy}`);
+ return response.data.claimedIssues ?? [];
+}
diff --git a/packages/web/src/pages/Honeypots/VaultDetailsPage/Sections/VaultSubmissionsSection/PublicSubmissionCard/components/SplitPointsActions.tsx b/packages/web/src/pages/Honeypots/VaultDetailsPage/Sections/VaultSubmissionsSection/PublicSubmissionCard/components/SplitPointsActions.tsx
new file mode 100644
index 000000000..6a4ac265b
--- /dev/null
+++ b/packages/web/src/pages/Honeypots/VaultDetailsPage/Sections/VaultSubmissionsSection/PublicSubmissionCard/components/SplitPointsActions.tsx
@@ -0,0 +1,173 @@
+import { GithubIssue, IClaimedIssue } from "@hats.finance/shared";
+import { IVault } from "@hats.finance/shared";
+import UploadIcon from "@mui/icons-material/FileUploadOutlined";
+import FlagIcon from "@mui/icons-material/OutlinedFlagOutlined";
+import { Button, HackerProfileImage, Loading, Pill, WithTooltip } from "components";
+import { useSiweAuth } from "hooks/siwe/useSiweAuth";
+import useConfirm from "hooks/useConfirm";
+import moment from "moment";
+import { useProfileByAddress } from "pages/HackerProfile/hooks";
+import { useAllTimeLeaderboard } from "pages/Leaderboard/LeaderboardPage/components/AllTimeLeaderboard/useAllTimeLeaderboard";
+import { useTranslation } from "react-i18next";
+import { IS_PROD, appChains } from "settings";
+import { useAccount, useNetwork } from "wagmi";
+import { useClaimIssue, useClaimedIssuesByVault } from "../hooks";
+import { StyledSplitPointsActions } from "./styles";
+
+export const getClaimedBy = (claimedIssue: IClaimedIssue | undefined) => {
+ if (!claimedIssue) return undefined;
+
+ const isExpired = moment(claimedIssue.expiresAt).isBefore(moment());
+ if (isExpired) return undefined;
+
+ return claimedIssue;
+};
+
+type SplitPointsActionsProps = {
+ vault: IVault;
+ submission: GithubIssue;
+};
+
+export const SplitPointsActions = ({ vault, submission }: SplitPointsActionsProps) => {
+ const { t } = useTranslation();
+ const { address } = useAccount();
+ const { chain } = useNetwork();
+ const { tryAuthentication } = useSiweAuth();
+ const confirm = useConfirm();
+
+ const { leaderboard } = useAllTimeLeaderboard("all", "streak");
+ const { data: profile } = useProfileByAddress(address);
+
+ const connectedChain = chain ? appChains[chain.id] : null;
+ const isTestnet = !IS_PROD && connectedChain?.chain.testnet;
+ const TOP_LEADERBOARD_PERCENTAGE = 0.2;
+ const TOP_LEADERBOARD_MIN_REWARDS = 5000;
+
+ const isConnected = !!address;
+ const isProfileCreated = !!profile;
+
+ const {
+ data: claimedIssues,
+ isLoading: isLoadingClaimedIssues,
+ refetch: refetchClaimedIssues,
+ } = useClaimedIssuesByVault(vault);
+ const claimIssue = useClaimIssue();
+
+ const leaderboardInBoundaries = leaderboard
+ .slice(0, Math.floor(leaderboard.length * TOP_LEADERBOARD_PERCENTAGE))
+ .filter((entry) => entry.totalAmount.usd >= TOP_LEADERBOARD_MIN_REWARDS);
+ const isInLeadearboardBoundaries = isTestnet
+ ? true
+ : leaderboardInBoundaries.find((entry) => entry.address.toLowerCase() === address?.toLowerCase());
+
+ const claimInfo = claimedIssues?.find((issue) => +issue.issueNumber === +submission.number);
+ const claimedByInfo = getClaimedBy(claimInfo);
+ const { data: claimedByProfile } = useProfileByAddress(claimedByInfo?.claimedBy);
+
+ const isClaimedByCurrentUser = claimedByInfo?.claimedBy.toLowerCase() === address?.toLowerCase();
+
+ const canExecuteAction = () => {
+ if (isClaimedByCurrentUser) return { can: true };
+ if (claimedByInfo) return { can: false, reason: t("issueAlreadyClaimed") };
+
+ if (!isConnected) return { can: false, reason: t("youNeedToConnectYourWallet") };
+ if (!isInLeadearboardBoundaries || !isProfileCreated) return { can: false, reason: t("youAreNotInTopLeaderboardPercentage") };
+ return { can: true };
+ };
+
+ const getActionButtonContent = () => {
+ if (isClaimedByCurrentUser)
+ return (
+ <>
+
+ Submit Fix & Test
+ >
+ );
+ if (submission.bonusPointsLabels.needsFix && submission.bonusPointsLabels.needsTest)
+ return (
+ <>
+
+ Claim Fix & Test
+ >
+ );
+ if (submission.bonusPointsLabels.needsFix)
+ return (
+ <>
+
+ Claim Fix
+ >
+ );
+
+ if (submission.bonusPointsLabels.needsTest)
+ return (
+ <>
+
+ Claim Test
+ >
+ );
+ return "";
+ };
+
+ const getClaimedByInfo = () => {
+ return (
+
+
Issue claimed by:
+
+
+
+ {claimedByProfile?.username}
+
+
+
+
+ );
+ };
+
+ const handleClaimIssue = async () => {
+ if (!canExecuteAction().can) return;
+
+ const wantsToClaim = await confirm({
+ title: t("claimIssue"),
+ titleIcon: ,
+ description: t("claimIssueDescription"),
+ cancelText: t("no"),
+ confirmText: t("yes"),
+ });
+
+ if (!wantsToClaim) return;
+
+ const signedIn = await tryAuthentication();
+ if (!signedIn) return;
+
+ await claimIssue.mutateAsync({ vault, issueNumber: submission.number });
+ refetchClaimedIssues();
+ };
+
+ if (isLoadingClaimedIssues) return null;
+
+ return (
+
+
+
+
+
+
+
+ {claimedByInfo && {getClaimedByInfo()}
}
+
+ {claimIssue.isLoading && }
+
+ );
+};
diff --git a/packages/web/src/pages/Honeypots/VaultDetailsPage/Sections/VaultSubmissionsSection/PublicSubmissionCard/components/styles.ts b/packages/web/src/pages/Honeypots/VaultDetailsPage/Sections/VaultSubmissionsSection/PublicSubmissionCard/components/styles.ts
new file mode 100644
index 000000000..365403535
--- /dev/null
+++ b/packages/web/src/pages/Honeypots/VaultDetailsPage/Sections/VaultSubmissionsSection/PublicSubmissionCard/components/styles.ts
@@ -0,0 +1,24 @@
+import styled from "styled-components";
+import { getSpacing } from "styles";
+
+export const StyledSplitPointsActions = styled.div`
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ flex-direction: row-reverse;
+ margin-top: ${getSpacing(1)};
+
+ .claimed-by-container {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ gap: ${getSpacing(1)};
+
+ .claimed-by-info,
+ .profile-container {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ }
+ }
+`;
diff --git a/packages/web/src/pages/Honeypots/VaultDetailsPage/Sections/VaultSubmissionsSection/PublicSubmissionCard/hooks.ts b/packages/web/src/pages/Honeypots/VaultDetailsPage/Sections/VaultSubmissionsSection/PublicSubmissionCard/hooks.ts
new file mode 100644
index 000000000..fb4b45747
--- /dev/null
+++ b/packages/web/src/pages/Honeypots/VaultDetailsPage/Sections/VaultSubmissionsSection/PublicSubmissionCard/hooks.ts
@@ -0,0 +1,40 @@
+import { IClaimedIssue, IVault } from "@hats.finance/shared";
+import { useMutation } from "@tanstack/react-query";
+import { UseMutationResult, useQuery } from "@tanstack/react-query";
+import { claimIssue, getClaimedIssuesByVault, getClaimedIssuesByVaultAndClaimedBy } from "./claimIssuesService";
+
+/**
+ * Gets claimed issues for a vault
+ */
+export const useClaimedIssuesByVault = (vault?: IVault) => {
+ return useQuery({
+ queryKey: ["claimed-issues-by-vault", vault?.id, vault?.chainId],
+ queryFn: () => getClaimedIssuesByVault(vault!),
+ enabled: !!vault,
+ });
+};
+
+/**
+ * Gets claimed issues for a vault and a claimed by
+ */
+export const useClaimedIssuesByVaultAndClaimedBy = (vault?: IVault, claimedBy?: string) => {
+ return useQuery({
+ queryKey: ["claimed-issues-by-vault-and-claimed-by", vault?.id, vault?.chainId, claimedBy],
+ queryFn: () => getClaimedIssuesByVaultAndClaimedBy(vault!, claimedBy!),
+ enabled: !!vault && !!claimedBy,
+ });
+};
+
+/**
+ * Upserts a profile
+ */
+export const useClaimIssue = (): UseMutationResult<
+ IClaimedIssue | undefined,
+ string,
+ { vault: IVault; issueNumber: number },
+ unknown
+> => {
+ return useMutation({
+ mutationFn: ({ vault, issueNumber }) => claimIssue(vault, issueNumber),
+ });
+};
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
index 965585cfe..c7b70effa 100644
--- a/packages/web/src/pages/Honeypots/VaultDetailsPage/Sections/VaultSubmissionsSection/PublicSubmissionCard/styles.ts
+++ b/packages/web/src/pages/Honeypots/VaultDetailsPage/Sections/VaultSubmissionsSection/PublicSubmissionCard/styles.ts
@@ -20,6 +20,12 @@ export const StyledPublicSubmissionCard = styled.div<{ isOpen: boolean }>(
border-color: var(--primary);
}
+ .labels {
+ display: flex;
+ gap: ${getSpacing(1)};
+ align-items: center;
+ }
+
.date {
position: absolute;
top: ${getSpacing(3)};
@@ -28,8 +34,13 @@ export const StyledPublicSubmissionCard = styled.div<{ isOpen: boolean }>(
.submission-title {
font-size: var(--small);
- padding-left: ${getSpacing(1)};
- font-weight: 700;
+ font-weight: 400;
+ margin-top: ${getSpacing(1.5)};
+
+ span {
+ font-weight: 700;
+ color: var(--secondary-light);
+ }
}
}
diff --git a/packages/web/src/pages/Honeypots/VaultDetailsPage/Sections/VaultSubmissionsSection/VaultSubmissionsSection.tsx b/packages/web/src/pages/Honeypots/VaultDetailsPage/Sections/VaultSubmissionsSection/VaultSubmissionsSection.tsx
index 0b2d51a53..814c6b7ec 100644
--- a/packages/web/src/pages/Honeypots/VaultDetailsPage/Sections/VaultSubmissionsSection/VaultSubmissionsSection.tsx
+++ b/packages/web/src/pages/Honeypots/VaultDetailsPage/Sections/VaultSubmissionsSection/VaultSubmissionsSection.tsx
@@ -1,5 +1,7 @@
import { IPayoutGraph, IVault } from "@hats.finance/shared";
+import ClockIcon from "@mui/icons-material/AccessTimeOutlined";
import OpenIcon from "@mui/icons-material/OpenInNewOutlined";
+import VerifiedIcon from "@mui/icons-material/VerifiedOutlined";
import { Alert, Button, HatSpinner } from "components";
import useConfirm from "hooks/useConfirm";
import { useTranslation } from "react-i18next";
@@ -19,7 +21,9 @@ export const VaultSubmissionsSection = ({ vault }: VaultSubmissionsSectionProps)
const { data: savedSubmissions, isLoading } = useSavedSubmissions(vault);
const { data: repoName } = useVaultRepoName(vault);
+
const isPrivateAudit = vault?.description?.["project-metadata"].isPrivateAudit;
+ const bonusPointsEnabled = vault.description?.["project-metadata"]?.bonusPointsEnabled;
const goToGithubIssues = async () => {
if (!repoName) return;
@@ -38,6 +42,28 @@ export const VaultSubmissionsSection = ({ vault }: VaultSubmissionsSectionProps)
window.open(githubLink, "_blank");
};
+ const getBonusPointsSection = () => {
+ if (isPrivateAudit) return null;
+ if (savedSubmissions?.length === 0) return null;
+
+ return (
+
+
+
+
+
+
{t("bonusPointsTitle")}
+
+
+
+
+
+
{t("bonusPointsReminder")}
+
+
+ );
+ };
+
return (
{isPrivateAudit && (
@@ -45,6 +71,7 @@ export const VaultSubmissionsSection = ({ vault }: VaultSubmissionsSectionProps)
{t("privateAuditSubmissionsOnlyOnGithub")}
)}
+ {bonusPointsEnabled && getBonusPointsSection()}
{t("submissions")}
@@ -66,7 +93,7 @@ export const VaultSubmissionsSection = ({ vault }: VaultSubmissionsSectionProps)
{!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
index 08171b6ac..13e1e67b3 100644
--- a/packages/web/src/pages/Honeypots/VaultDetailsPage/Sections/VaultSubmissionsSection/styles.ts
+++ b/packages/web/src/pages/Honeypots/VaultDetailsPage/Sections/VaultSubmissionsSection/styles.ts
@@ -10,4 +10,44 @@ export const StyledSubmissionsSection = styled.div`
flex-direction: column;
gap: ${getSpacing(3)};
}
+
+ .bonus-points-section {
+ border: 2px solid var(--primary);
+ padding: ${getSpacing(3)} ${getSpacing(4)};
+ margin-bottom: ${getSpacing(4)};
+
+ .title-container {
+ display: flex;
+ align-items: center;
+ gap: ${getSpacing(1)};
+ margin-bottom: ${getSpacing(2)};
+ font-size: var(--moderate-big);
+ font-weight: 600;
+
+ .icon {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: ${getSpacing(4.5)};
+ height: ${getSpacing(4.5)};
+ padding: ${getSpacing(1)};
+ border-radius: 50%;
+ background-color: var(--primary);
+ }
+ }
+
+ .content {
+ ul {
+ line-height: 1.5;
+ padding-left: ${getSpacing(3)};
+ }
+ }
+
+ .remember {
+ display: flex;
+ align-items: center;
+ margin-top: ${getSpacing(2)};
+ gap: ${getSpacing(1)};
+ }
+ }
`;
diff --git a/packages/web/src/pages/Honeypots/VaultDetailsPage/hooks.ts b/packages/web/src/pages/Honeypots/VaultDetailsPage/hooks.ts
index f6c543acb..386176bae 100644
--- a/packages/web/src/pages/Honeypots/VaultDetailsPage/hooks.ts
+++ b/packages/web/src/pages/Honeypots/VaultDetailsPage/hooks.ts
@@ -1,18 +1,18 @@
-import { IVault } from "@hats.finance/shared";
+import { GithubIssue, IVault } from "@hats.finance/shared";
import { UseMutationResult, UseQueryResult, useQuery } from "@tanstack/react-query";
import { AxiosError } from "axios";
+import { getGithubIssuesFromVault } from "pages/CommitteeTools/SubmissionsTool/submissionsService.api";
import { useAccount, useMutation } from "wagmi";
import * as messageSignaturesService from "./messageSignaturesService";
import * as savedSubmissionsService from "./savedSubmissionsService";
-import { IGithubIssue } from "./types";
/**
* Returns all saved submissions for a vault
*/
-export const useSavedSubmissions = (vault: IVault | undefined): UseQueryResult => {
+export const useSavedSubmissions = (vault: IVault | undefined): UseQueryResult => {
return useQuery({
queryKey: ["github-issues", vault?.id],
- queryFn: () => savedSubmissionsService.getSavedSubmissions(vault?.id),
+ queryFn: () => getGithubIssuesFromVault(vault!),
refetchOnWindowFocus: false,
enabled: !!vault,
});
diff --git a/packages/web/src/pages/Honeypots/VaultDetailsPage/savedSubmissionsService.ts b/packages/web/src/pages/Honeypots/VaultDetailsPage/savedSubmissionsService.ts
index e32f47b00..00a085451 100644
--- a/packages/web/src/pages/Honeypots/VaultDetailsPage/savedSubmissionsService.ts
+++ b/packages/web/src/pages/Honeypots/VaultDetailsPage/savedSubmissionsService.ts
@@ -5,7 +5,7 @@ import { IGithubIssue } from "./types";
/**
* Get all saved github issues for a vault
*/
-export async function getSavedSubmissions(vaultId: string | undefined): Promise {
+export async function getGHSubmissions(vaultId: string | undefined): Promise {
if (!vaultId) return [];
try {
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 f9e8735ba..71a1d680e 100644
--- a/packages/web/src/pages/Submissions/SubmissionFormPage/FormSteps/SubmissionDescriptions/SubmissionDescriptions.tsx
+++ b/packages/web/src/pages/Submissions/SubmissionFormPage/FormSteps/SubmissionDescriptions/SubmissionDescriptions.tsx
@@ -1,7 +1,9 @@
-import { ISubmissionMessageObject, IVulnerabilitySeverity } from "@hats.finance/shared";
+import { GithubIssue, ISubmissionMessageObject, IVulnerabilitySeverity } from "@hats.finance/shared";
import { yupResolver } from "@hookform/resolvers/yup";
import AddIcon from "@mui/icons-material/AddOutlined";
+import CloseIcon from "@mui/icons-material/CloseOutlined";
import RemoveIcon from "@mui/icons-material/DeleteOutlined";
+import FlagIcon from "@mui/icons-material/OutlinedFlagOutlined";
import {
Alert,
Button,
@@ -10,13 +12,25 @@ import {
FormSelectInput,
FormSelectInputOption,
FormSupportFilesInput,
+ Loading,
+ Pill,
WithTooltip,
} from "components";
import download from "downloadjs";
import { getCustomIsDirty, useEnhancedForm } from "hooks/form";
+import moment from "moment";
+import { getGithubIssuesFromVault } from "pages/CommitteeTools/SubmissionsTool/submissionsService.api";
+import { useProfileByAddress } from "pages/HackerProfile/hooks";
+import { useClaimedIssuesByVaultAndClaimedBy } from "pages/Honeypots/VaultDetailsPage/Sections/VaultSubmissionsSection/PublicSubmissionCard/hooks";
+import { getVaultRepoName } from "pages/Honeypots/VaultDetailsPage/savedSubmissionsService";
+import { HoneypotsRoutePaths } from "pages/Honeypots/router";
import { useContext, useEffect, useState } from "react";
import { Controller, useFieldArray, useWatch } from "react-hook-form";
import { useTranslation } from "react-i18next";
+import { useNavigate } from "react-router-dom";
+import { searchFileInHatsRepo } from "utils/github.utils";
+import { slugify } from "utils/slug.utils";
+import { useAccount } from "wagmi";
import { encryptWithHatsKey, encryptWithKeys } from "../../encrypt";
import { SUBMISSION_INIT_DATA, SubmissionFormContext } from "../../store";
import { ISubmissionsDescriptionsData } from "../../types";
@@ -26,14 +40,33 @@ import { getAuditSubmissionTexts, getBountySubmissionTexts } from "./utils";
export function SubmissionDescriptions() {
const { t } = useTranslation();
+ const { address } = useAccount();
+ const navigate = useNavigate();
+
+ const { data: hackerProfile } = useProfileByAddress(address);
const { submissionData, setSubmissionData, vault, allFormDisabled } = useContext(SubmissionFormContext);
const [severitiesOptions, setSeveritiesOptions] = useState();
const isAuditSubmission = vault?.description?.["project-metadata"].type === "audit";
const isPrivateAudit = vault?.description?.["project-metadata"].isPrivateAudit;
+ const bonusPointsEnabled = vault?.description?.["project-metadata"].bonusPointsEnabled;
+
+ const { data: claimedIssues, isLoading: isLoadingClaimedIssues } = useClaimedIssuesByVaultAndClaimedBy(vault, address);
+
+ const [vaultGithubIssuesOpts, setVaultGithubIssuesOpts] = useState();
+ const [vaultGithubIssues, setVaultGithubIssues] = useState(undefined);
+ const [isLoadingGH, setIsLoadingGH] = useState(false);
- const { register, handleSubmit, control, reset, setValue } = useEnhancedForm({
+ const {
+ register,
+ handleSubmit,
+ control,
+ reset,
+ setValue,
+ getValues,
+ formState: { errors },
+ } = useEnhancedForm({
resolver: yupResolver(getCreateDescriptionSchema(t)),
mode: "onChange",
});
@@ -42,7 +75,6 @@ export function SubmissionDescriptions() {
append: appendSubmissionDescription,
remove: removeSubmissionDescription,
} = useFieldArray({ control, name: `descriptions` });
-
const watchDescriptions = useWatch({ control, name: `descriptions` });
const controlledDescriptions = fields.map((field, index) => {
return {
@@ -75,6 +107,11 @@ export function SubmissionDescriptions() {
if (!vault || !vault.description || !vault.description.severities) return;
for (const [idx, description] of controlledDescriptions.entries()) {
+ if (description.type === "complement") {
+ if (description.isEncrypted === true) setValue(`descriptions.${idx}.isEncrypted`, false);
+ continue;
+ }
+
const severitySelected =
vault.description?.severities &&
(vault.description.severities as IVulnerabilitySeverity[]).find((sev) => sev.name.toLowerCase() === description.severity);
@@ -97,6 +134,38 @@ export function SubmissionDescriptions() {
}
}, [controlledDescriptions, vault, setValue, isAuditSubmission]);
+ // Get information from github
+ const someComplementSubmission = controlledDescriptions.some((desc) => desc.type === "complement");
+ useEffect(() => {
+ if (!someComplementSubmission) return;
+ if (!vault) return;
+ if (!claimedIssues) return;
+ if (vaultGithubIssues !== undefined || isLoadingGH) return;
+ const loadGhIssues = async () => {
+ setIsLoadingGH(true);
+ const ghIssues = await getGithubIssuesFromVault(vault);
+ const ghIssuesOpts = ghIssues
+ .filter((ghIssue) =>
+ claimedIssues?.some((ci) => +ci.issueNumber === +ghIssue.number && !moment(ci.expiresAt).isBefore(moment()))
+ )
+ .filter((ghIssue) => ghIssue.bonusPointsLabels.needsFix || ghIssue.bonusPointsLabels.needsTest)
+ .map((ghIssue) => ({
+ label: `[#${ghIssue.number}] ${ghIssue.title}`,
+ value: `${ghIssue.number}`,
+ }));
+
+ setVaultGithubIssuesOpts(ghIssuesOpts);
+ setVaultGithubIssues(ghIssues);
+ setIsLoadingGH(false);
+ };
+ loadGhIssues();
+ }, [vault, vaultGithubIssues, isLoadingGH, someComplementSubmission, claimedIssues, address]);
+
+ useEffect(() => {
+ setVaultGithubIssuesOpts(undefined);
+ setVaultGithubIssues(undefined);
+ }, [address]);
+
const handleSaveAndDownloadDescription = async (formData: ISubmissionsDescriptionsData) => {
if (!vault) return;
if (!submissionData) return alert("Please fill previous steps first.");
@@ -118,7 +187,7 @@ export function SubmissionDescriptions() {
if (keyOrKeys.length === 0) return alert("This project has no keys to encrypt the description. Please contact HATS team.");
const getSubmissionTextsFunction = isAuditSubmission ? getAuditSubmissionTexts : getBountySubmissionTexts;
- const submissionTexts = getSubmissionTextsFunction(submissionData, formData.descriptions);
+ const submissionTexts = getSubmissionTextsFunction(submissionData, formData.descriptions, hackerProfile);
const toEncrypt = submissionTexts.toEncrypt;
const decrypted = submissionTexts.decrypted;
@@ -165,81 +234,405 @@ export function SubmissionDescriptions() {
if (!vault) return {t("Submissions.firstYouNeedToSelectAProject")};
- return (
-
- {controlledDescriptions.map((submissionDescription, index) => {
- return (
-
-
-
- {t("issue")} #{index + 1}
-
-
-
- {submissionDescription.isEncrypted
- ? t("Submissions.encryptedSubmission")
- : t("Submissions.decryptedSubmission")}
-
-
-
- {t("Submissions.provideExplanation")}
+ const getSubmissionsRoute = () => {
+ const mainRoute = `/${isAuditSubmission ? HoneypotsRoutePaths.audits : HoneypotsRoutePaths.bugBounties}`;
+ const vaultSlug = slugify(vault.description?.["project-metadata"].name ?? "");
-
-
{
+ return (
+ <>
+ {t("Submissions.provideExplanation")}
+
+
+ (
+ (field.name, dirtyFields, defaultValues)}
+ error={error}
+ label={t("severity")}
+ placeholder={t("severityPlaceholder")}
colorable
+ options={severitiesOptions ?? []}
+ {...field}
/>
- (
- (field.name, dirtyFields, defaultValues)}
- error={error}
- label={t("severity")}
- placeholder={t("severityPlaceholder")}
- colorable
- options={severitiesOptions ?? []}
- {...field}
- />
- )}
- />
+ )}
+ />
+
+
+ (
+ (field.name, dirtyFields, defaultValues)}
+ error={error}
+ colorable
+ {...field}
+ />
+ )}
+ />
+
+ {/* {!submissionDescription.isEncrypted && !allFormDisabled && (
+ (
+
+ )}
+ />
+ )} */}
+
+ Does this issue needs a fix?
+
+
setValue(`descriptions.${index}.isTestApplicable`, true, { shouldValidate: true })}
+ >
+
+
+
{t("Submissions.pocIsApplicable")}
+
+
+
+
setValue(`descriptions.${index}.isTestApplicable`, false, { shouldValidate: true })}
+ >
+
+
+
{t("Submissions.pocIsNotApplicable")}
+
+
+ >
+ );
+ };
+
+ const getComplementIssueForm = (submissionDescription: (typeof controlledDescriptions)[number], index: number) => {
+ const { complementGhIssue, needsFix, needsTest } = submissionDescription;
+
+ return (
+ <>
+ {t("Submissions.selectIssueToComplement")}
+ {(!vaultGithubIssuesOpts || vaultGithubIssuesOpts?.length === 0) && (
+ <>
+
+ {t("Submissions.firstYouNeedToClaimSomeIssues")}
+
+
+
+ >
+ )}
+ {vaultGithubIssuesOpts && vaultGithubIssuesOpts?.length > 0 && (
+ <>
(
- (field.name, dirtyFields, defaultValues)}
error={error}
+ label={t("Submissions.githubIssue")}
+ placeholder={t("Submissions.selectGithubIssue")}
colorable
+ options={vaultGithubIssuesOpts ?? []}
{...field}
+ value={field.value ?? ""}
+ onChange={(a) => {
+ field.onChange(a);
+ const ghIssue = vaultGithubIssues?.find((gh) => gh.number === Number(a as string));
+ if (ghIssue) {
+ setValue(`descriptions.${index}.complementGhIssue`, ghIssue);
+ setValue(`descriptions.${index}.needsFix`, ghIssue.bonusPointsLabels.needsFix);
+ setValue(`descriptions.${index}.needsTest`, ghIssue.bonusPointsLabels.needsTest);
+ }
+ }}
/>
)}
/>
- {!submissionDescription.isEncrypted && !allFormDisabled && (
- (
-
- )}
- />
+ {/* labels */}
+
+
+ {complementGhIssue?.bonusPointsLabels.needsFix &&
}
+ {complementGhIssue?.bonusPointsLabels.needsTest &&
}
+
+
+
+ {/* Fix PR section */}
+ {needsFix && (
+ <>
+ {t("Submissions.addFixFiles")}:
+ {t("Submissions.addFixFilesExplanation")}
+ {
+ if (!a?.length) return;
+ for (const file of a) {
+ const fixFiles = getValues(`descriptions.${index}.complementFixFiles`);
+ if (fixFiles.some((f) => f.file.ipfsHash === file.ipfsHash)) continue;
+ const newFile = { file, path: "", pathOpts: [] };
+
+ const repoName = await getVaultRepoName(vault?.id);
+ if (!repoName) {
+ setValue(`descriptions.${index}.complementFixFiles`, [...fixFiles, newFile]);
+ return;
+ }
+
+ const pathOptions = await searchFileInHatsRepo(repoName, file.name);
+ if (pathOptions.length === 0) {
+ setValue(`descriptions.${index}.complementFixFiles`, [...fixFiles, newFile]);
+ } else if (pathOptions.length === 1) {
+ setValue(`descriptions.${index}.complementFixFiles`, [
+ ...fixFiles,
+ { ...newFile, path: pathOptions[0], pathOpts: [] },
+ ]);
+ } else {
+ setValue(`descriptions.${index}.complementFixFiles`, [
+ ...fixFiles,
+ { ...newFile, path: "", pathOpts: pathOptions },
+ ]);
+ }
+ }
+ }}
+ />
+
+
+
+ {(submissionDescription.complementFixFiles ?? []).map((item, idx) => (
+
+
+
+
{
+ const fixFiles = getValues(`descriptions.${index}.complementFixFiles`);
+ setValue(
+ `descriptions.${index}.complementFixFiles`,
+ fixFiles.filter((_, fileIndex) => fileIndex !== idx)
+ );
+ }}
+ />
+ {item.file?.name}
+
+
+
+
{t("Submissions.filePath")}:
+
+
+
+
+
+
+
+ {item.pathOpts?.map((opt) => (
+
setValue(`descriptions.${index}.complementFixFiles.${idx}.path`, opt)}>
+
+
+ ))}
+
+
+ ))}
+
+
+ >
)}
+
+
+ {/* Test PR section */}
+ {!submissionDescription.testNotApplicable && needsTest && (
+ <>
+ {t("Submissions.addTestFiles")}:
+ {t("Submissions.addTestFilesExplanation")}
+ {
+ if (!a?.length) return;
+ for (const file of a) {
+ const testFiles = getValues(`descriptions.${index}.complementTestFiles`);
+ if (testFiles.some((f) => f.file.ipfsHash === file.ipfsHash)) continue;
+ const newFile = { file, path: "", pathOpts: [] };
+
+ const repoName = await getVaultRepoName(vault?.id);
+ if (!repoName) {
+ setValue(`descriptions.${index}.complementTestFiles`, [...testFiles, newFile]);
+ return;
+ }
+
+ const pathOptions = await searchFileInHatsRepo(repoName, file.name);
+ if (pathOptions.length === 0) {
+ setValue(`descriptions.${index}.complementTestFiles`, [...testFiles, newFile]);
+ } else if (pathOptions.length === 1) {
+ setValue(`descriptions.${index}.complementTestFiles`, [
+ ...testFiles,
+ { ...newFile, path: pathOptions[0], pathOpts: [] },
+ ]);
+ } else {
+ setValue(`descriptions.${index}.complementTestFiles`, [
+ ...testFiles,
+ { ...newFile, path: "", pathOpts: pathOptions },
+ ]);
+ }
+ }
+ }}
+ />
+
+ {/* {t("Submissions.filesAttached")}:
*/}
+
+
+ {(submissionDescription.complementTestFiles ?? []).map((item, idx) => (
+
+
+
+
{
+ const testFiles = getValues(`descriptions.${index}.complementTestFiles`);
+ setValue(
+ `descriptions.${index}.complementTestFiles`,
+ testFiles.filter((_, fileIndex) => fileIndex !== idx)
+ );
+ }}
+ />
+ {item.file?.name}
+
+
+
+
{t("Submissions.filePath")}:
+
+
+
+
+
+
+
+ {item.pathOpts?.map((opt) => (
+
setValue(`descriptions.${index}.complementTestFiles.${idx}.path`, opt)}>
+
+
+ ))}
+
+
+ ))}
+
+
+ >
+ )}
+ {needsTest && needsFix && (
+
+
+
+ )}
+ >
+ )}
+ >
+ );
+ };
+
+ return (
+
+ {controlledDescriptions.map((submissionDescription, index) => {
+ return (
+
+
+
+ {t("submission")} #{index + 1}
+
+ {((submissionDescription.type === "new" && submissionDescription.severity) ||
+ submissionDescription.type === "complement") && (
+
+
+ {submissionDescription.isEncrypted
+ ? t("Submissions.encryptedSubmission")
+ : t("Submissions.decryptedSubmission")}
+
+
+ )}
+
+
+ {bonusPointsEnabled && (
+
+
setValue(`descriptions.${index}.type`, "new")}>
+
+
+
+
+
setValue(`descriptions.${index}.type`, "complement")}>
+
+
+
{t("complementSubmission")}
+
+
+
+ )}
+
+ {submissionDescription.type === "new"
+ ? getNewIssueForm(submissionDescription, index)
+ : getComplementIssueForm(submissionDescription, index)}
+
{controlledDescriptions.length > 1 && !allFormDisabled && (
+
+ {(isLoadingClaimedIssues || isLoadingGH) && }
);
}
diff --git a/packages/web/src/pages/Submissions/SubmissionFormPage/FormSteps/SubmissionDescriptions/formSchema.ts b/packages/web/src/pages/Submissions/SubmissionFormPage/FormSteps/SubmissionDescriptions/formSchema.ts
index 8ae22ea9e..a909197ab 100644
--- a/packages/web/src/pages/Submissions/SubmissionFormPage/FormSteps/SubmissionDescriptions/formSchema.ts
+++ b/packages/web/src/pages/Submissions/SubmissionFormPage/FormSteps/SubmissionDescriptions/formSchema.ts
@@ -5,13 +5,77 @@ export const getCreateDescriptionSchema = (intl: TFunction) =>
Yup.object().shape({
descriptions: Yup.array().of(
Yup.object({
- title: Yup.string()
- .min(5, intl("min-characters", { min: 5 }))
- .required(intl("required")),
- severity: Yup.string().required(intl("required")),
- description: Yup.string()
- .min(20, intl("min-characters", { min: 20 }))
- .required(intl("required")),
+ type: Yup.string().required(intl("required")),
+
+ // new fields
+ title: Yup.string().when("type", (type: "new" | "complement", schema: any) => {
+ if (type === "complement") return schema;
+ return schema.min(5, intl("min-characters", { min: 5 })).required(intl("required"));
+ }),
+ severity: Yup.string().when("type", (type: "new" | "complement", schema: any) => {
+ if (type === "complement") return schema;
+ return schema.required(intl("required"));
+ }),
+ description: Yup.string().when("type", (type: "new" | "complement", schema: any) => {
+ if (type === "complement") return schema;
+ return schema.min(20, intl("min-characters", { min: 20 })).required(intl("required"));
+ }),
+ isTestApplicable: Yup.boolean().when("type", (type: "new" | "complement", schema: any) => {
+ if (type === "new") return schema.required(intl("required"));
+ return schema;
+ }),
+
+ // complement fields
+ needsFix: Yup.boolean(),
+ needsTest: Yup.boolean(),
+ testNotApplicable: Yup.boolean(),
+ complementGhIssueNumber: Yup.string().when("type", (type: "new" | "complement", schema: any) => {
+ if (type === "new") return schema;
+ return schema.required(intl("required"));
+ }),
+ complementGhIssue: Yup.object().when("type", (type: "new" | "complement", schema: any) => {
+ if (type === "new") return schema;
+ return schema.required(intl("required"));
+ }),
+ complementFixFiles: Yup.array()
+ .of(
+ Yup.object({
+ file: Yup.object().required(intl("required")),
+ path: Yup.string()
+ .required(intl("required"))
+ .test("pathEndsWithFileNameError", intl("pathEndsWithFileNameError"), (val, ctx: any) => {
+ const fileName = ctx.from[0].value.file?.name;
+ return val?.endsWith(fileName) ?? false;
+ }),
+ })
+ )
+ .test("min", intl("required"), (val, ctx: any) => {
+ const type = ctx.from[0].value.type;
+ const needsFix = ctx.from[0].value.needsFix;
+ if (type === "new") return true;
+ if (!needsFix) return true;
+ return (val?.length ?? 0) > 0 ?? false;
+ }),
+ complementTestFiles: Yup.array()
+ .of(
+ Yup.object({
+ file: Yup.object().required(intl("required")),
+ path: Yup.string()
+ .required(intl("required"))
+ .test("pathEndsWithFileNameError", intl("pathEndsWithFileNameError"), (val, ctx: any) => {
+ const fileName = ctx.from[0].value.file?.name;
+ return val?.endsWith(fileName) ?? false;
+ }),
+ })
+ )
+ .test("min", intl("required"), (val, ctx: any) => {
+ const type = ctx.from[0].value.type;
+ const testNotApplicable = ctx.from[0].value.testNotApplicable;
+ const needsTest = ctx.from[0].value.needsTest;
+ if (type === "new") return true;
+ if (testNotApplicable || !needsTest) return true;
+ return (val?.length ?? 0) > 0 ?? false;
+ }),
})
),
});
diff --git a/packages/web/src/pages/Submissions/SubmissionFormPage/FormSteps/SubmissionDescriptions/styles.ts b/packages/web/src/pages/Submissions/SubmissionFormPage/FormSteps/SubmissionDescriptions/styles.ts
index 76388068e..95c8834ef 100644
--- a/packages/web/src/pages/Submissions/SubmissionFormPage/FormSteps/SubmissionDescriptions/styles.ts
+++ b/packages/web/src/pages/Submissions/SubmissionFormPage/FormSteps/SubmissionDescriptions/styles.ts
@@ -46,9 +46,164 @@ export const StyledSubmissionDescription = styled.div<{ isEncrypted: boolean }>(
}
}
+ .gh-labels {
+ display: flex;
+ flex-direction: column;
+ gap: ${getSpacing(1)};
+ margin-bottom: ${getSpacing(2.5)};
+ margin-top: ${getSpacing(-1.5)};
+
+ .labels {
+ display: flex;
+ gap: ${getSpacing(1)};
+ }
+ }
+
+ .options {
+ display: flex;
+ gap: ${getSpacing(3)};
+ width: 100%;
+
+ .option {
+ display: flex;
+ gap: ${getSpacing(2)};
+ cursor: pointer;
+ transition: all 0.2s;
+
+ &:hover {
+ opacity: 0.8;
+ }
+
+ .check-circle {
+ width: ${getSpacing(2.5)};
+ height: ${getSpacing(2.5)};
+ aspect-ratio: 1;
+ border-radius: 50%;
+ border: 2px solid var(--primary);
+
+ &.selected {
+ position: relative;
+
+ &::after {
+ content: "";
+ width: ${getSpacing(1.2)};
+ height: ${getSpacing(1.2)};
+ border-radius: 50%;
+ top: 50%;
+ left: 50%;
+ transform: translate(-43%, -50%);
+ position: absolute;
+ background-color: var(--primary);
+ display: block;
+ }
+ }
+
+ &.error {
+ border-color: var(--error-red);
+
+ &::after {
+ background-color: var(--error-red);
+ }
+ }
+ }
+ .info {
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ gap: ${getSpacing(0.5)};
+ }
+ }
+ }
+
+ .files-attached-container {
+ display: flex;
+ align-items: center;
+ gap: ${getSpacing(1)};
+ margin-top: ${getSpacing(3)};
+
+ .files {
+ display: flex;
+ align-items: center;
+ gap: ${getSpacing(2)};
+ flex-wrap: wrap;
+ flex-direction: column;
+ width: 100%;
+
+ li {
+ list-style: none;
+ display: flex;
+ width: 100%;
+ align-items: flex-start;
+ flex-direction: column;
+
+ .file-container {
+ display: flex;
+ gap: ${getSpacing(5)};
+ width: 100%;
+ align-items: flex-start;
+
+ .file {
+ display: flex;
+ align-items: center;
+ gap: ${getSpacing(1)};
+ border: 1px solid var(--primary);
+ border-radius: 50px;
+ padding: ${getSpacing(0.5)} ${getSpacing(1)};
+ font-size: var(--xxsmall);
+ margin-top: ${getSpacing(2)};
+
+ .remove-icon {
+ cursor: pointer;
+ transition: 0.1s;
+
+ &:hover {
+ color: var(--error-red);
+ }
+ }
+ }
+
+ .file-path {
+ display: flex;
+ align-items: flex-start;
+ gap: ${getSpacing(2)};
+ width: 100%;
+
+ p {
+ white-space: nowrap;
+ margin-top: ${getSpacing(2.5)};
+ }
+
+ .file-path-input {
+ width: 100%;
+ }
+ }
+ }
+
+ .file-opts {
+ display: flex;
+ gap: ${getSpacing(0.5)};
+ flex-wrap: wrap;
+
+ div {
+ cursor: pointer;
+ transition: 0.2s;
+
+ &:hover {
+ opacity: 0.8;
+ }
+ }
+ }
+ }
+ }
+ }
+
.buttons {
display: flex;
justify-content: flex-end;
}
+
+ .error-text {
+ color: var(--error-red);
+ }
`
);
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 3ad0cccd1..fb97f35da 100644
--- a/packages/web/src/pages/Submissions/SubmissionFormPage/FormSteps/SubmissionDescriptions/utils.ts
+++ b/packages/web/src/pages/Submissions/SubmissionFormPage/FormSteps/SubmissionDescriptions/utils.ts
@@ -1,4 +1,6 @@
+import { IPFS_PREFIX } from "constants/constants";
import { BASE_SERVICE_URL } from "settings";
+import { IHackerProfile } from "types";
import { ISubmissionData, ISubmissionsDescriptionsData } from "../../types";
export const SUBMISSION_DESCRIPTION_TEMPLATE = `**Description**\\
@@ -20,7 +22,8 @@ Describe how the vulnerability can be exploited.
export const getAuditSubmissionTexts = (
submissionData: ISubmissionData,
- descriptions: ISubmissionsDescriptionsData["descriptions"]
+ descriptions: ISubmissionsDescriptionsData["descriptions"],
+ hackerProfile: IHackerProfile | undefined
) => {
const toEncrypt = `**Communication channel:** ${submissionData.contact?.communicationChannel} (${
submissionData.contact?.communicationChannelType
@@ -29,10 +32,18 @@ export const getAuditSubmissionTexts = (
${descriptions
.filter((description) => description.isEncrypted)
- .map(
- (description, idx) => `
+ .map((description, idx) =>
+ description.type === "new"
+ ? `
## [ISSUE #${idx + 1}]: ${description.title} (${description.severity})\n
${description.description.trim()}
+##`
+ : `
+## [ISSUE #${idx + 1}]: COMPLEMENTARY [Issue #${description.complementGhIssueNumber}] ${description.complementGhIssue?.title}\n\n
+### Fix files
+${description.complementFixFiles.map((file) => ` - ${file.path} (${IPFS_PREFIX}/${file.file.ipfsHash})`).join("\n")}\n
+### Test files
+${description.complementTestFiles.map((file) => ` - ${file.path} (${IPFS_PREFIX}/${file.file.ipfsHash})`).join("\n")}
##`
)
.join("\n")}`;
@@ -40,12 +51,15 @@ ${description.description.trim()}
const decrypted = `**Project Name:** ${submissionData.project?.projectName}\n
**Project Id:** ${submissionData.project?.projectId}\n
**Github username:** ${submissionData.contact?.githubUsername || "---"}\n
-**Twitter username:** ${submissionData.contact?.twitterUsername || "---"}
+**Twitter username:** ${submissionData.contact?.twitterUsername || "---"}\n
+**HATS Profile:** ${hackerProfile ? `${hackerProfile?.username}` : "---"}\n
+**Beneficiary:** ${submissionData.contact?.beneficiary}
${descriptions
.filter((description) => !description.isEncrypted)
- .map(
- (description, idx) => `
+ .map((description, idx) =>
+ description.type === "new"
+ ? `
## [ISSUE #${idx + 1}]: ${description.title} (${description.severity})\n
${description.description.trim()}
${
@@ -53,6 +67,13 @@ ${
? `**Files:**\n${description.files.map((file) => ` - ${file.name} (${BASE_SERVICE_URL}/files/${file.ipfsHash})`).join("\n")}`
: ""
}
+##`
+ : `
+## [ISSUE #${idx + 1}]: COMPLEMENTARY [Issue #${description.complementGhIssueNumber}] ${description.complementGhIssue?.title}\n\n
+### Fix files
+${description.complementFixFiles.map((file) => ` - ${file.path} (${IPFS_PREFIX}/${file.file.ipfsHash})`).join("\n")}\n
+### Test files ${description.testNotApplicable ? "(NOT APPLICABLE) " : ""}
+${description.complementTestFiles.map((file) => ` - ${file.path} (${IPFS_PREFIX}/${file.file.ipfsHash})`).join("\n")}
##`
)
.join("\n")}`;
@@ -68,11 +89,13 @@ ${
export const getBountySubmissionTexts = (
submissionData: ISubmissionData,
- descriptions: ISubmissionsDescriptionsData["descriptions"]
+ descriptions: ISubmissionsDescriptionsData["descriptions"],
+ hackerProfile: IHackerProfile | undefined
) => {
const toEncrypt = `**Project Name:** ${submissionData.project?.projectName}\n
**Project Id:** ${submissionData.project?.projectId}\n
**Beneficiary:** ${submissionData.contact?.beneficiary}\n
+**HATS Profile:** ${hackerProfile ? `${hackerProfile?.username}` : "---"}\n
**Communication channel:** ${submissionData.contact?.communicationChannel} (${submissionData.contact?.communicationChannelType})
${descriptions
@@ -95,21 +118,42 @@ ${description.description.trim()}
export const getGithubIssueDescription = (
submissionData: ISubmissionData,
- description: ISubmissionsDescriptionsData["descriptions"][0]
+ description: ISubmissionsDescriptionsData["descriptions"][0],
+ hackerProfile: IHackerProfile | undefined
) => {
- return `${submissionData.ref === "audit-wizard" ? "***Submitted via auditwizard.io***\n" : ""}
-**Github username:** ${submissionData.contact?.githubUsername ? `@${submissionData.contact?.githubUsername}` : "--"}
+ if (description.type === "new") {
+ return `${submissionData.ref === "audit-wizard" ? "***Submitted via auditwizard.io***\n" : ""}
+ **Github username:** ${submissionData.contact?.githubUsername ? `@${submissionData.contact?.githubUsername}` : "--"}
+ **Twitter username:** ${submissionData.contact?.twitterUsername ? `${submissionData.contact?.twitterUsername}` : "--"}
+ **HATS Profile:** ${hackerProfile ? `${hackerProfile?.username}` : "---"}
+ **Beneficiary:** ${submissionData.contact?.beneficiary}
+ **Submission hash (on-chain):** ${submissionData.submissionResult?.transactionHash}
+ **Severity:** ${description.severity}
+
+ **Description:**
+ ${description.description.trim()}
+
+ ${
+ description.files && description.files.length > 0
+ ? `**Files:**\n${description.files
+ .map((file) => ` - ${file.name} (${BASE_SERVICE_URL}/files/${file.ipfsHash})`)
+ .join("\n")}`
+ : ""
+ }
+ `;
+ }
+
+ return `**Github username:** ${submissionData.contact?.githubUsername ? `@${submissionData.contact?.githubUsername}` : "--"}
**Twitter username:** ${submissionData.contact?.twitterUsername ? `${submissionData.contact?.twitterUsername}` : "--"}
+**HATS Profile:** ${hackerProfile ? `${hackerProfile?.username}` : "---"}
+**Beneficiary:** ${submissionData.contact?.beneficiary}
**Submission hash (on-chain):** ${submissionData.submissionResult?.transactionHash}
-**Severity:** ${description.severity}
**Description:**
-${description.description.trim()}
-
-${
- description.files && description.files.length > 0
- ? `**Files:**\n${description.files.map((file) => ` - ${file.name} (${BASE_SERVICE_URL}/files/${file.ipfsHash})`).join("\n")}`
- : ""
-}
+### Fix files
+${description.complementFixFiles.map((file) => ` - ${file.path} (${IPFS_PREFIX}/${file.file.ipfsHash})`).join("\n")}\n
+### Test files ${description.testNotApplicable ? "(NOT APPLICABLE) " : ""}
+${description.complementTestFiles.map((file) => ` - ${file.path} (${IPFS_PREFIX}/${file.file.ipfsHash})`).join("\n")}
+##
`;
};
diff --git a/packages/web/src/pages/Submissions/SubmissionFormPage/SubmissionFormPage.tsx b/packages/web/src/pages/Submissions/SubmissionFormPage/SubmissionFormPage.tsx
index 136bb7507..9a354272b 100644
--- a/packages/web/src/pages/Submissions/SubmissionFormPage/SubmissionFormPage.tsx
+++ b/packages/web/src/pages/Submissions/SubmissionFormPage/SubmissionFormPage.tsx
@@ -1,4 +1,3 @@
-import { IVulnerabilitySeverity } from "@hats.finance/shared";
import ErrorIcon from "@mui/icons-material/ErrorOutlineOutlined";
import ClearIcon from "@mui/icons-material/HighlightOffOutlined";
import { Button, Loading, Seo } from "components";
@@ -6,6 +5,7 @@ import { LocalStorage } from "constants/constants";
import { LogClaimContract } from "contracts";
import { useVaults } from "hooks/subgraph/vaults/useVaults";
import useConfirm from "hooks/useConfirm";
+import { useProfileByAddress } from "pages/HackerProfile/hooks";
import { useUserHasCollectedSignature } from "pages/Honeypots/VaultDetailsPage/hooks";
import { HoneypotsRoutePaths } from "pages/Honeypots/router";
import { calcCid } from "pages/Submissions/SubmissionFormPage/encrypt";
@@ -15,7 +15,7 @@ import { useNavigate, useSearchParams } from "react-router-dom";
import { IS_PROD } from "settings";
import { getAppVersion } from "utils";
import { slugify } from "utils/slug.utils";
-import { useNetwork, useWaitForTransaction } from "wagmi";
+import { useAccount, useNetwork, useWaitForTransaction } from "wagmi";
import {
SubmissionContactInfo,
SubmissionDescriptions,
@@ -42,6 +42,9 @@ export const SubmissionFormPage = () => {
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const { chain } = useNetwork();
+ const { address } = useAccount();
+ const { data: hackerProfile } = useProfileByAddress(address);
+
const [currentStep, setCurrentStep] = useState();
const [submissionData, setSubmissionData] = useState();
const [allFormDisabled, setAllFormDisabled] = useState(false);
@@ -51,6 +54,7 @@ export const SubmissionFormPage = () => {
const vault = (activeVaults ?? []).find((vault) => vault.id === submissionData?.project?.projectId);
const requireMessageSignature = vault?.description?.["project-metadata"].requireMessageSignature;
+
const { data: userHasCollectedSignature, isLoading: isLoadingCollectedSignature } = useUserHasCollectedSignature(vault);
const steps = useMemo(
@@ -59,7 +63,7 @@ export const SubmissionFormPage = () => {
{ title: t("Submissions.termsAndProcess"), component: SubmissionTermsAndProcess, card: SubmissionStep.terms },
{ title: t("Submissions.communicationChannel"), component: SubmissionContactInfo, card: SubmissionStep.contact },
{
- title: t("Submissions.describeVulnerability"),
+ title: t("Submissions.submissionDescription"),
component: SubmissionDescriptions,
card: SubmissionStep.submissionsDescriptions,
},
@@ -168,7 +172,7 @@ export const SubmissionFormPage = () => {
});
try {
- const res = await submitVulnerabilitySubmission(data, vault);
+ const res = await submitVulnerabilitySubmission(data, vault, hackerProfile);
if (res.success) {
setSubmissionData({
@@ -187,7 +191,7 @@ export const SubmissionFormPage = () => {
});
}
},
- [vault]
+ [vault, hackerProfile]
);
const submitSubmission = useCallback(async () => {
@@ -300,20 +304,19 @@ export const SubmissionFormPage = () => {
verified: false,
submission: "",
submissionMessage: "",
- descriptions: auditWizardSubmission.submissionsDescriptions.descriptions.map((desc: any) => {
- const severity = (vault.description?.severities as IVulnerabilitySeverity[]).find(
- (sev) =>
- desc.severity.toLowerCase()?.includes(sev.name.toLowerCase()) ||
- sev.name.toLowerCase()?.includes(desc.severity.toLowerCase())
- );
- return {
- title: desc.title,
- description: desc.description,
- severity: severity?.name.toLowerCase() ?? desc.severity.toLowerCase(),
- isEncrypted: !severity?.decryptSubmissions,
- files: [],
- };
- }),
+ descriptions: auditWizardSubmission.submissionsDescriptions.descriptions.map((desc: any) => ({
+ type: "new", // or "complement" based on your logic
+ complementTestFiles: [],
+ complementFixFiles: [],
+ title: desc.title,
+ description: desc.description,
+ severity: desc.severity,
+ isEncrypted: desc.isEncrypted,
+ files: desc.files || [],
+ testNotApplicable: false,
+ needsFix: false,
+ needsTest: false,
+ })),
},
submissionResult: undefined,
}));
@@ -397,7 +400,7 @@ export const SubmissionFormPage = () => {
- {isLoadingCollectedSignature && }
+ {requireMessageSignature && isLoadingCollectedSignature && }
>
);
};
diff --git a/packages/web/src/pages/Submissions/SubmissionFormPage/store.ts b/packages/web/src/pages/Submissions/SubmissionFormPage/store.ts
index 303723edf..6a250ca92 100644
--- a/packages/web/src/pages/Submissions/SubmissionFormPage/store.ts
+++ b/packages/web/src/pages/Submissions/SubmissionFormPage/store.ts
@@ -1,7 +1,7 @@
import { IVault } from "@hats.finance/shared";
import { Dispatch, SetStateAction, createContext } from "react";
import { SUBMISSION_DESCRIPTION_TEMPLATE } from "./FormSteps/SubmissionDescriptions/utils";
-import { ISubmissionData } from "./types";
+import { ISubmissionData, ISubmissionsDescriptionsData } from "./types";
const packageJSON = require("../../../../package.json");
@@ -29,13 +29,20 @@ export const SUBMISSION_INIT_DATA = {
submission: "",
descriptions: [
{
+ type: "new",
isEncrypted: true,
title: "",
description: SUBMISSION_DESCRIPTION_TEMPLATE,
severity: "",
files: [],
sessionKey: "" as any,
+ complementFixFiles: [],
+ complementTestFiles: [],
+ testNotApplicable: false,
+ isTestApplicable: undefined,
+ needsFix: false,
+ needsTest: false,
},
],
- },
+ } satisfies ISubmissionsDescriptionsData,
};
diff --git a/packages/web/src/pages/Submissions/SubmissionFormPage/submissionsService.api.ts b/packages/web/src/pages/Submissions/SubmissionFormPage/submissionsService.api.ts
index 8344ad3a0..e5629f423 100644
--- a/packages/web/src/pages/Submissions/SubmissionFormPage/submissionsService.api.ts
+++ b/packages/web/src/pages/Submissions/SubmissionFormPage/submissionsService.api.ts
@@ -1,4 +1,4 @@
-import { IVault } from "@hats.finance/shared";
+import { GithubIssue, IHackerProfile, IVault } from "@hats.finance/shared";
import { axiosClient } from "config/axiosClient";
import { auditWizardVerifyService } from "constants/constants";
import { BASE_SERVICE_URL } from "settings";
@@ -14,12 +14,15 @@ import { IAuditWizardSubmissionData, ISubmissionData, ISubmitSubmissionRequest }
*/
export async function submitVulnerabilitySubmission(
submissionData: ISubmissionData,
- vault: IVault
+ vault: IVault,
+ hackerProfile: IHackerProfile | undefined
): Promise<{ success: boolean; auditCompetitionRepo?: string }> {
if (!submissionData.project || !submissionData.submissionsDescriptions || !submissionData.submissionResult) {
throw new Error(`Invalid params on 'submitVulnerabilitySubmission' function: ${submissionData}`);
}
+ const bonusPointsEnabled = vault?.description?.["project-metadata"]?.bonusPointsEnabled;
+
const submissionRequest: ISubmitSubmissionRequest = {
submitVulnerabilityRequest: {
chainId: submissionData.submissionResult.chainId,
@@ -31,11 +34,28 @@ export async function submitVulnerabilitySubmission(
createIssueRequests:
vault.description?.["project-metadata"].type === "audit"
? submissionData.submissionsDescriptions.descriptions
- ?.filter((desc) => !desc.isEncrypted)
+ ?.filter((desc) => !desc.isEncrypted && desc.type === "new")
?.map((description) => ({
issueTitle: description.title,
- issueDescription: getGithubIssueDescription(submissionData, description),
+ issueDescription: getGithubIssueDescription(submissionData, description, hackerProfile),
issueFiles: description.files?.map((file) => file.ipfsHash),
+ isTestApplicable: description.isTestApplicable,
+ bonusPointsEnabled,
+ }))
+ : [],
+ createPRsRequests:
+ vault.description?.["project-metadata"].type === "audit"
+ ? submissionData.submissionsDescriptions.descriptions
+ ?.filter((desc) => !desc.isEncrypted && desc.type === "complement")
+ ?.map((description) => ({
+ pullRequestTitle: `Complementary submission for #${description.complementGhIssueNumber}`,
+ pullRequestDescription: getGithubIssueDescription(submissionData, description, hackerProfile),
+ pullRequestFiles: [...description.complementFixFiles, ...description.complementTestFiles].map((file) => ({
+ path: file.path,
+ fileIpfsHash: file.file.ipfsHash,
+ })),
+ githubIssue: description.complementGhIssue as GithubIssue,
+ githubIssueNumber: Number(description.complementGhIssueNumber),
}))
: [],
};
diff --git a/packages/web/src/pages/Submissions/SubmissionFormPage/types.ts b/packages/web/src/pages/Submissions/SubmissionFormPage/types.ts
index 14309f77d..6adc99eba 100644
--- a/packages/web/src/pages/Submissions/SubmissionFormPage/types.ts
+++ b/packages/web/src/pages/Submissions/SubmissionFormPage/types.ts
@@ -1,3 +1,4 @@
+import { GithubIssue } from "@hats.finance/shared";
import { ISavedFile } from "components";
import { SessionKey } from "openpgp";
@@ -29,12 +30,25 @@ export interface ISubmissionsDescriptionsData {
submission: string; // Submission object ({encrypted: string, decrypted: string})
submissionMessage: string; // It's only for showing on the frontend final step
descriptions: {
+ type: "new" | "complement"; // "new" is for new vulnerabilities, "complement" is for fix/test submissions
+
+ // complement fields
+ testNotApplicable: boolean;
+ complementTestFiles: { file: ISavedFile; path: string; pathOpts: string[] }[];
+ complementFixFiles: { file: ISavedFile; path: string; pathOpts: string[] }[];
+ complementGhIssueNumber?: string;
+ complementGhIssue?: GithubIssue;
+ needsFix: boolean;
+ needsTest: boolean;
+
+ // new fields
title: string;
description: string;
severity: string;
files: ISavedFile[];
sessionKey?: SessionKey;
isEncrypted?: boolean;
+ isTestApplicable?: boolean;
}[];
}
@@ -80,6 +94,15 @@ export interface ISubmitSubmissionRequest {
issueTitle: string;
issueDescription: string;
issueFiles: string[];
+ isTestApplicable?: boolean;
+ bonusPointsEnabled?: boolean;
+ }[];
+ createPRsRequests: {
+ pullRequestTitle: string;
+ pullRequestDescription: string;
+ pullRequestFiles: { path: string; fileIpfsHash: string }[];
+ githubIssue: GithubIssue;
+ githubIssueNumber?: number;
}[];
}
@@ -119,8 +142,8 @@ export const getCurrentAuditwizardSubmission = (
...awSubmission.submissionsDescriptions,
descriptions:
form.submissionsDescriptions?.descriptions.map((d, idx) => ({
- title: d.title,
- description: d.description,
+ title: d.title ?? "",
+ description: d.description ?? "",
severity: awSubmission.submissionsDescriptions.descriptions[idx].severity,
})) ?? [],
},
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 437a5e228..2ac9c2b0d 100644
--- a/packages/web/src/pages/VaultEditor/VaultEditorFormPage/SetupSteps/VaultDetailsForm/VaultDetailsForm.tsx
+++ b/packages/web/src/pages/VaultEditor/VaultEditorFormPage/SetupSteps/VaultDetailsForm/VaultDetailsForm.tsx
@@ -99,6 +99,10 @@ export function VaultDetailsForm() {
if (isAudit) setValue("usingPointingSystem", true);
else setValue("usingPointingSystem", false);
trigger("vulnerability-severities-spec");
+
+ // Change the bonus points enabled value
+ if (vaultType === "audit") setValue("project-metadata.bonusPointsEnabled", true);
+ else setValue("project-metadata.bonusPointsEnabled", false);
});
return (
@@ -159,6 +163,15 @@ export function VaultDetailsForm() {
label={t("requireMessageSignature")}
/>
) : null}
+ {isAdvancedMode && vaultType === "audit" && (
+
+ )}
diff --git a/packages/web/src/pages/VaultEditor/VaultEditorFormPage/formSchema.ts b/packages/web/src/pages/VaultEditor/VaultEditorFormPage/formSchema.ts
index f440d22e4..f22d3cbeb 100644
--- a/packages/web/src/pages/VaultEditor/VaultEditorFormPage/formSchema.ts
+++ b/packages/web/src/pages/VaultEditor/VaultEditorFormPage/formSchema.ts
@@ -27,6 +27,7 @@ export const getEditedDescriptionYupSchema = (intl: TFunction) =>
isPrivateAudit: Yup.boolean(),
isContinuousAudit: Yup.boolean(),
requireMessageSignature: Yup.boolean(),
+ bonusPointsEnabled: Yup.boolean(),
messageToSign: Yup.string().when("requireMessageSignature", (requireMessageSignature: boolean, schema: any) => {
if (requireMessageSignature) return schema.required(intl("required")).typeError(intl("required"));
}),
diff --git a/packages/web/src/utils/github.utils.ts b/packages/web/src/utils/github.utils.ts
index 39c77d4d7..d9c6cb770 100644
--- a/packages/web/src/utils/github.utils.ts
+++ b/packages/web/src/utils/github.utils.ts
@@ -1,4 +1,20 @@
+import { axiosClient } from "config/axiosClient";
+import { BASE_SERVICE_URL } from "settings";
+
export const isGithubUsernameValid = async (username: string) => {
const response = await fetch(`https://api.github.com/users/${username}`);
return response.status === 200;
};
+
+export const searchFileInHatsRepo = async (repoName: string, fileName: string): Promise
=> {
+ if (!repoName || !fileName) return [];
+
+ try {
+ const res = await axiosClient.get(`${BASE_SERVICE_URL}/utils/search-file-in-repo?repoName=${repoName}&fileName=${fileName}`);
+
+ return res.data.files ?? [];
+ } catch (err) {
+ console.error(err);
+ return [];
+ }
+};