diff --git a/packages/shared/package.json b/packages/shared/package.json
index b00161c5c..048c6b0a9 100644
--- a/packages/shared/package.json
+++ b/packages/shared/package.json
@@ -1,6 +1,6 @@
{
"name": "@hats.finance/shared",
- "version": "1.1.119",
+ "version": "1.1.120",
"description": "",
"main": "dist/index.js",
"types": "dist/index.d.ts",
diff --git a/packages/shared/src/types/payout.ts b/packages/shared/src/types/payout.ts
index e2ca01aab..ac9407c7a 100644
--- a/packages/shared/src/types/payout.ts
+++ b/packages/shared/src/types/payout.ts
@@ -42,6 +42,18 @@ export interface IPayoutResponse {
updatedAt?: Date;
}
+export type GithubIssue = {
+ id: number;
+ number: number;
+ title: string;
+ createdBy: number;
+ labels: string[];
+ validLabels: string[];
+ createdAt: string;
+ body: string;
+ txHash?: string;
+};
+
export type IPayoutData = ISinglePayoutData | ISplitPayoutData;
interface IPayoutDataBase {
@@ -64,6 +76,7 @@ export interface ISinglePayoutData extends IPayoutDataBase {
nftUrl: string;
submissionData?: { id: string; subId: string; idx: number };
decryptedSubmission?: Omit; // Omit: workaround to avoid circular dependency;
+ ghIssue?: GithubIssue;
}
// Only for v2 vaults
@@ -89,6 +102,7 @@ export interface ISplitPayoutBeneficiary {
nftUrl: string;
submissionData?: { id: string; subId: string; idx: number };
decryptedSubmission?: Omit; // Omit: workaround to avoid circular dependency;
+ ghIssue?: GithubIssue;
}
export interface IPayoutSignature {
diff --git a/packages/shared/src/utils/vaultEditor.utils.ts b/packages/shared/src/utils/vaultEditor.utils.ts
index b553b0c20..7ab474adc 100644
--- a/packages/shared/src/utils/vaultEditor.utils.ts
+++ b/packages/shared/src/utils/vaultEditor.utils.ts
@@ -447,9 +447,9 @@ export function editedFormToCreateVaultOnChainCall(
descriptionHash,
bountyGovernanceHAT: formatPercentage(getHatsFee()),
bountyHackerHATVested: formatPercentage(editedVaultDescription.parameters.fixedHatsRewardPercetange ?? 0),
- arbitratorCanChangeBeneficiary: true,
- arbitratorCanChangeBounty: true,
- arbitratorCanSubmitClaims: true,
+ arbitratorCanChangeBeneficiary: false,
+ arbitratorCanChangeBounty: false,
+ arbitratorCanSubmitClaims: false,
isTokenLockRevocable: false,
arbitrator: ChainsConfig[+(editedVaultDescription.committee.chainId ?? "1")].arbitratorContract,
} as ICreateVaultOnChainCall;
diff --git a/packages/web/src/components/FormControls/FormSelectInput/FormSelectInput.tsx b/packages/web/src/components/FormControls/FormSelectInput/FormSelectInput.tsx
index d34f06a87..beac7bd49 100644
--- a/packages/web/src/components/FormControls/FormSelectInput/FormSelectInput.tsx
+++ b/packages/web/src/components/FormControls/FormSelectInput/FormSelectInput.tsx
@@ -18,6 +18,7 @@ export interface FormSelectInputOption {
interface FormSelectInputProps {
name?: string;
label?: string;
+ helper?: string;
placeholder?: string;
emptyState?: string;
multiple?: boolean;
@@ -52,6 +53,7 @@ export function FormSelectInputComponent(
noSelectedMark = false,
emptyState,
error,
+ helper,
placeholder,
label,
}: FormSelectInputProps,
@@ -145,6 +147,7 @@ export function FormSelectInputComponent(
})}
)}
+ {!error && helper && {helper}}
);
}
diff --git a/packages/web/src/components/FormControls/FormSelectInput/styles.ts b/packages/web/src/components/FormControls/FormSelectInput/styles.ts
index 533a2b700..79df730a5 100644
--- a/packages/web/src/components/FormControls/FormSelectInput/styles.ts
+++ b/packages/web/src/components/FormControls/FormSelectInput/styles.ts
@@ -24,6 +24,14 @@ export const StyledFormSelectInput = styled.div<{ noMargin: boolean; flexExpand:
margin-left: ${getSpacing(1)};
font-size: var(--xxsmall);
}
+
+ span.helper {
+ display: block;
+ color: var(--secondary-light);
+ margin-top: ${getSpacing(0.5)};
+ margin-left: ${getSpacing(1)};
+ font-size: var(--xxsmall);
+ }
`
);
diff --git a/packages/web/src/constants/constants.ts b/packages/web/src/constants/constants.ts
index dceeca576..beece24fb 100644
--- a/packages/web/src/constants/constants.ts
+++ b/packages/web/src/constants/constants.ts
@@ -9,6 +9,7 @@ export enum LocalStorage {
ShowedWhereverCTA = "HATS_SHOWED_WHEREVER_CTA",
Submissions = "HATS_SUBMISSIONS",
SelectedSubmissions = "HATS_USER_SELECTED_SUBMISSIONS",
+ SelectedVaultInSubmissions = "HATS_USER_SELECTED_VAULT_IN_SUBMISSIONS",
CoingeckoPrices = "HATS_COINGECKO_PRICES",
CompetitionStreak = "HATS_HAS_SEEN_COMPETITION_STREAK",
AirdropModalSeen = "HATS_HAS_SEEN_AIRDROP_MODAL",
diff --git a/packages/web/src/hooks/vaults/useUserVaults.tsx b/packages/web/src/hooks/vaults/useUserVaults.tsx
index f0467840f..51676121d 100644
--- a/packages/web/src/hooks/vaults/useUserVaults.tsx
+++ b/packages/web/src/hooks/vaults/useUserVaults.tsx
@@ -73,6 +73,9 @@ export const useUserVaults = (versions: UserVaultsVersion[] = ["all"]) => {
}
}
+ foundVaults.sort(
+ (a, b) => (b.description?.["project-metadata"].starttime ?? 0) - (a.description?.["project-metadata"].starttime ?? 0)
+ );
setUserVaults(foundVaults);
setIsLoading(false);
};
diff --git a/packages/web/src/languages/en.json b/packages/web/src/languages/en.json
index fa196c616..1ba27ca65 100644
--- a/packages/web/src/languages/en.json
+++ b/packages/web/src/languages/en.json
@@ -730,6 +730,11 @@
"roles": "Roles",
"competitionsRewards": "Competitions rewards",
"earnedFees": "Earned fees",
+ "loadingGithubIssues": "Loading GitHub issues",
+ "submittedAs": "Submitted as",
+ "labeledAs": "Labeled as",
+ "onlyShowLabeledIssues": "Show only labeled issues",
+ "ghIssue": "GH issue",
"MyWallet": {
"overview": "Overview",
"pointValue": "Point value",
@@ -1125,7 +1130,10 @@
"notAllowedToCreatePayout": "You are not allowed to create a payout for this vault. Please verify you are connected with a committee wallet.",
"filterBySeverity": "Filter by severity",
"filterByTitle": "Filter by title",
- "title": "Title"
+ "filterByVault": "Filter by vault",
+ "title": "Title",
+ "showAllVaults": "Show all vaults",
+ "goToVaultGithubIssues": "Go to vault GitHub issues"
},
"Leaderboard": {
"ifYouAreAWinner": "If you are one of the winners and you want to claim your rewards, please execute the claim by clicking on the button:",
diff --git a/packages/web/src/pages/CommitteeTools/PayoutsTool/PayoutFormPage/forms/SinglePayout/SinglePayoutForm.tsx b/packages/web/src/pages/CommitteeTools/PayoutsTool/PayoutFormPage/forms/SinglePayout/SinglePayoutForm.tsx
index ffe0219d7..82485fbc5 100644
--- a/packages/web/src/pages/CommitteeTools/PayoutsTool/PayoutFormPage/forms/SinglePayout/SinglePayoutForm.tsx
+++ b/packages/web/src/pages/CommitteeTools/PayoutsTool/PayoutFormPage/forms/SinglePayout/SinglePayoutForm.tsx
@@ -1,17 +1,19 @@
import {
DefaultIndexArray,
+ GithubIssue,
IPayoutData,
ISinglePayoutData,
IVulnerabilitySeverityV1,
IVulnerabilitySeverityV2,
} from "@hats.finance/shared";
-import { FormInput, FormSelectInput, Spinner } from "components";
+import { FormInput, FormSelectInput, Loading, Spinner } from "components";
import { getCustomIsDirty, useEnhancedFormContext } from "hooks/form";
import { useOnChange } from "hooks/usePrevious";
import { hasSubmissionData } from "pages/CommitteeTools/PayoutsTool/utils/hasSubmissionData";
import { SubmissionCard } from "pages/CommitteeTools/SubmissionsTool/SubmissionsListPage/SubmissionCard";
+import { getGhIssueFromSubmission, getGithubIssuesFromVault } from "pages/CommitteeTools/SubmissionsTool/submissionsService.api";
import { useVaultSubmissionsByKeystore } from "pages/CommitteeTools/SubmissionsTool/submissionsService.hooks";
-import { useContext } from "react";
+import { useContext, useEffect, useState } from "react";
import { Controller, useWatch } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { SinglePayoutAllocation } from "../../../components";
@@ -81,6 +83,23 @@ export const SinglePayoutForm = () => {
}
});
+ const [vaultGithubIssues, setVaultGithubIssues] = useState(undefined);
+ const [isLoadingGH, setIsLoadingGH] = useState(false);
+
+ // Get information from github
+ useEffect(() => {
+ if (!beneficiarySubmission || !vault) return;
+ if (vaultGithubIssues !== undefined) return;
+
+ const loadGhIssues = async () => {
+ setIsLoadingGH(true);
+ const ghIssues = await getGithubIssuesFromVault(vault);
+ setVaultGithubIssues(ghIssues);
+ setIsLoadingGH(false);
+ };
+ loadGhIssues();
+ }, [vault, vaultGithubIssues, beneficiarySubmission]);
+
return (
@@ -100,6 +119,10 @@ export const SinglePayoutForm = () => {
) : (
@@ -203,6 +226,7 @@ export const SinglePayoutForm = () => {
colorable
/>
*/}
+ {isLoadingGH && }
);
};
diff --git a/packages/web/src/pages/CommitteeTools/PayoutsTool/components/PayoutAllocation/SplitPayoutAllocation/components/SplitPayoutBeneficiaryForm.tsx b/packages/web/src/pages/CommitteeTools/PayoutsTool/components/PayoutAllocation/SplitPayoutAllocation/components/SplitPayoutBeneficiaryForm.tsx
index b7bf9ab1c..a40e2c697 100644
--- a/packages/web/src/pages/CommitteeTools/PayoutsTool/components/PayoutAllocation/SplitPayoutAllocation/components/SplitPayoutBeneficiaryForm.tsx
+++ b/packages/web/src/pages/CommitteeTools/PayoutsTool/components/PayoutAllocation/SplitPayoutAllocation/components/SplitPayoutBeneficiaryForm.tsx
@@ -1,15 +1,16 @@
-import { IPayoutResponse, ISplitPayoutData, IVault } from "@hats.finance/shared";
+import { GithubIssue, IPayoutResponse, ISplitPayoutData, IVault } from "@hats.finance/shared";
import DeleteIcon from "@mui/icons-material/DeleteOutlineOutlined";
import InfoIcon from "@mui/icons-material/InfoOutlined";
import MoreIcon from "@mui/icons-material/MoreVertOutlined";
-import { DropdownSelector, FormInput, FormSelectInput, FormSelectInputOption, Modal, Spinner } from "components";
+import { DropdownSelector, FormInput, FormSelectInput, FormSelectInputOption, Loading, Modal, Spinner } from "components";
import { getCustomIsDirty, useEnhancedFormContext } from "hooks/form";
import useModal from "hooks/useModal";
import { useOnChange } from "hooks/usePrevious";
import { hasSubmissionData } from "pages/CommitteeTools/PayoutsTool/utils/hasSubmissionData";
import { SubmissionCard } from "pages/CommitteeTools/SubmissionsTool/SubmissionsListPage/SubmissionCard";
+import { getGhIssueFromSubmission, getGithubIssuesFromVault } from "pages/CommitteeTools/SubmissionsTool/submissionsService.api";
import { useVaultSubmissionsByKeystore } from "pages/CommitteeTools/SubmissionsTool/submissionsService.hooks";
-import { useState } from "react";
+import { useEffect, useState } from "react";
import { Controller, UseFieldArrayRemove, useWatch } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { SinglePayoutAllocation } from "../../SinglePayoutAllocation/SinglePayoutAllocation";
@@ -97,6 +98,23 @@ export const SplitPayoutBeneficiaryForm = ({
setValue(`beneficiaries.${index}.percentageOfPayout` as any, defaultPoints, { shouldValidate: true });
});
+ const [vaultGithubIssues, setVaultGithubIssues] = useState(undefined);
+ const [isLoadingGH, setIsLoadingGH] = useState(false);
+
+ // Get information from github
+ useEffect(() => {
+ if (!beneficiarySubmission || !vault) return;
+ if (vaultGithubIssues !== undefined) return;
+
+ const loadGhIssues = async () => {
+ setIsLoadingGH(true);
+ const ghIssues = await getGithubIssuesFromVault(vault);
+ setVaultGithubIssues(ghIssues);
+ setIsLoadingGH(false);
+ };
+ loadGhIssues();
+ }, [vault, vaultGithubIssues, beneficiarySubmission]);
+
const getMoreOptions = () => {
if (beneficiariesCount === undefined) return [];
if (beneficiariesCount > 1 && !readOnly && !isPayoutCreated) {
@@ -148,6 +166,10 @@ export const SplitPayoutBeneficiaryForm = ({
submission={
isPayoutCreated ? beneficiaries[index]?.decryptedSubmission ?? beneficiarySubmission! : beneficiarySubmission!
}
+ ghIssue={getGhIssueFromSubmission(
+ isPayoutCreated ? beneficiaries[index]?.decryptedSubmission ?? beneficiarySubmission! : beneficiarySubmission!,
+ vaultGithubIssues
+ )}
/>
) : (
@@ -241,6 +263,8 @@ export const SplitPayoutBeneficiaryForm = ({
/>
+
+ {isLoadingGH && }
);
};
diff --git a/packages/web/src/pages/CommitteeTools/SubmissionsTool/SubmissionDetailsPage/SubmissionDetailsPage.tsx b/packages/web/src/pages/CommitteeTools/SubmissionsTool/SubmissionDetailsPage/SubmissionDetailsPage.tsx
index cf8cbdd18..6553d0978 100644
--- a/packages/web/src/pages/CommitteeTools/SubmissionsTool/SubmissionDetailsPage/SubmissionDetailsPage.tsx
+++ b/packages/web/src/pages/CommitteeTools/SubmissionsTool/SubmissionDetailsPage/SubmissionDetailsPage.tsx
@@ -1,17 +1,19 @@
-import { allowedElementsMarkdown } from "@hats.finance/shared";
+import { GithubIssue, 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 { Alert, Button, HatSpinner, Loading, WalletButton } from "components";
import { useKeystore } from "components/Keystore";
import { IPFS_PREFIX } from "constants/constants";
+import { useVaults } from "hooks/subgraph/vaults/useVaults";
import { RoutePaths } from "navigation";
-import { useEffect } from "react";
+import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate, useParams } from "react-router-dom";
import { appChains } from "settings";
import { useAccount } from "wagmi";
import { SubmissionCard } from "../SubmissionsListPage/SubmissionCard";
+import { getGhIssueFromSubmission, getGithubIssuesFromVault } from "../submissionsService.api";
import { useVaultSubmissionsByKeystore } from "../submissionsService.hooks";
import { StyledSubmissionDetailsPage } from "./styles";
@@ -20,15 +22,36 @@ export const SubmissionDetailsPage = () => {
const navigate = useNavigate();
const { address } = useAccount();
const { keystore, initKeystore } = useKeystore();
+ const { allVaults, vaultsReadyAllChains } = useVaults();
const { subId } = useParams();
const { data: committeeSubmissions, isLoading } = useVaultSubmissionsByKeystore();
const submission = committeeSubmissions?.find((sub) => sub.subId === subId);
+ const [vaultGithubIssues, setVaultGithubIssues] = useState(undefined);
+ const [isLoadingGH, setIsLoadingGH] = useState(false);
+
useEffect(() => {
if (!keystore) setTimeout(() => initKeystore(), 600);
}, [keystore, initKeystore]);
+ // Get information from github
+ useEffect(() => {
+ if (!submission) return;
+
+ const vault = allVaults?.find((vault) => vault.id.toLowerCase() === submission.linkedVault?.id.toLowerCase());
+ if (!vault) return;
+ if (vaultGithubIssues !== undefined) return;
+
+ const loadGhIssues = async () => {
+ setIsLoadingGH(true);
+ const ghIssues = await getGithubIssuesFromVault(vault);
+ setVaultGithubIssues(ghIssues);
+ setIsLoadingGH(false);
+ };
+ loadGhIssues();
+ }, [allVaults, vaultGithubIssues, submission]);
+
const openSubmissionData = () => {
window.open(`${IPFS_PREFIX}/${submission?.submissionHash}`, "_blank");
};
@@ -82,7 +105,11 @@ export const SubmissionDetailsPage = () => {
-
+
{
)}
>
)}
+
+ {!vaultsReadyAllChains && }
+ {isLoadingGH && }
);
};
diff --git a/packages/web/src/pages/CommitteeTools/SubmissionsTool/SubmissionsListPage/SubmissionCard.tsx b/packages/web/src/pages/CommitteeTools/SubmissionsTool/SubmissionsListPage/SubmissionCard.tsx
index 49f522c08..2bd4a89d2 100644
--- a/packages/web/src/pages/CommitteeTools/SubmissionsTool/SubmissionsListPage/SubmissionCard.tsx
+++ b/packages/web/src/pages/CommitteeTools/SubmissionsTool/SubmissionsListPage/SubmissionCard.tsx
@@ -1,4 +1,4 @@
-import { ISubmittedSubmission, IVulnerabilitySeverity, parseSeverityName } from "@hats.finance/shared";
+import { GithubIssue, ISubmittedSubmission, IVulnerabilitySeverity, parseSeverityName } from "@hats.finance/shared";
import ArrowIcon from "@mui/icons-material/ArrowForwardOutlined";
import BoxUnselected from "@mui/icons-material/CheckBoxOutlineBlankOutlined";
import BoxSelected from "@mui/icons-material/CheckBoxOutlined";
@@ -18,11 +18,13 @@ type SubmissionCardProps = {
inPayout?: boolean;
isChecked?: boolean;
onCheckChange?: (submission: ISubmittedSubmission) => void;
+ ghIssue?: GithubIssue;
};
export const SubmissionCard = ({
submission,
onCheckChange,
+ ghIssue,
noActions = false,
isChecked = false,
inPayout = false,
@@ -65,11 +67,27 @@ export const SubmissionCard = ({
{submissionData?.severity && (
+ {ghIssue && (
+
+ {t("ghIssue")} #{ghIssue.number} -
+
+ )}
+ {t("submittedAs")}:
+ {ghIssue && ghIssue?.validLabels.length > 0 && (
+ <>
+ {t("labeledAs")}:
+
+ >
+ )}
)}
{submissionData?.title}
diff --git a/packages/web/src/pages/CommitteeTools/SubmissionsTool/SubmissionsListPage/SubmissionsListPage.tsx b/packages/web/src/pages/CommitteeTools/SubmissionsTool/SubmissionsListPage/SubmissionsListPage.tsx
index 1f38e9bd1..e4dfb205c 100644
--- a/packages/web/src/pages/CommitteeTools/SubmissionsTool/SubmissionsListPage/SubmissionsListPage.tsx
+++ b/packages/web/src/pages/CommitteeTools/SubmissionsTool/SubmissionsListPage/SubmissionsListPage.tsx
@@ -1,8 +1,10 @@
import {
+ GithubIssue,
IPayoutData,
ISinglePayoutData,
ISplitPayoutData,
ISubmittedSubmission,
+ IVault,
IVaultDescriptionV2,
IVulnerabilitySeverity,
PayoutType,
@@ -18,14 +20,25 @@ import CalendarIcon from "@mui/icons-material/CalendarTodayOutlined";
import BoxUnselected from "@mui/icons-material/CheckBoxOutlineBlankOutlined";
import BoxSelected from "@mui/icons-material/CheckBoxOutlined";
import ClearIcon from "@mui/icons-material/ClearOutlined";
-import DownloadIcon from "@mui/icons-material/FileDownloadOutlined";
import KeyIcon from "@mui/icons-material/KeyOutlined";
+import OpenIcon from "@mui/icons-material/OpenInNewOutlined";
import RescanIcon from "@mui/icons-material/ReplayOutlined";
import SearchIcon from "@mui/icons-material/SearchOutlined";
import SyncIcon from "@mui/icons-material/SyncOutlined";
import PayoutIcon from "@mui/icons-material/TollOutlined";
import { AxiosError } from "axios";
-import { Alert, Button, FormDateInput, FormInput, FormSelectInput, HatSpinner, Loading, Modal, WalletButton } from "components";
+import {
+ Alert,
+ Button,
+ FormDateInput,
+ FormInput,
+ FormSelectInput,
+ FormSelectInputOption,
+ HatSpinner,
+ Loading,
+ Modal,
+ WalletButton,
+} from "components";
import { useKeystore } from "components/Keystore";
import { IndexedDBs } from "config/DBConfig";
import { LocalStorage } from "constants/constants";
@@ -34,12 +47,16 @@ import { useVaults } from "hooks/subgraph/vaults/useVaults";
import useConfirm from "hooks/useConfirm";
import moment from "moment";
import { RoutePaths } from "navigation";
+import { useVaultRepoName } from "pages/Honeypots/VaultDetailsPage/hooks";
import { useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { useIndexedDB } from "react-indexed-db-hook";
import { useLocation, useNavigate } from "react-router-dom";
+import { appChains } from "settings";
+import { shortenIfAddress } from "utils/addresses.utils";
import { getVaultCurator } from "utils/curator.utils";
import { useAccount } from "wagmi";
+import { getGhIssueFromSubmission, getGithubIssuesFromVault } from "../submissionsService.api";
import { useCreatePayoutFromSubmissions, useVaultSubmissionsByKeystore } from "../submissionsService.hooks";
import { SubmissionCard } from "./SubmissionCard";
import { StyledSubmissionsListPage } from "./styles";
@@ -59,8 +76,33 @@ export const SubmissionsListPage = () => {
const [openDateFilter, setOpenDateFilter] = useState(false);
const [dateFilter, setDateFilter] = useState({ from: 0, to: 0, active: false });
- const [severityFilter, setSeverityFilter] = useState
();
+ const [severityFilter, setSeverityFilter] = useState("all");
const [titleFilter, setTitleFilter] = useState("");
+ const [vaultFilter, setVaultFilter] = useState(localStorage.getItem(LocalStorage.SelectedVaultInSubmissions) ?? "");
+ const [onlyShowLabeled, setOnlyShowLabeled] = useState(false);
+
+ const filteredVault = allVaults?.find((vault) => vault.id.toLowerCase() === vaultFilter.toLowerCase());
+ const { data: filteredVaultRepoName } = useVaultRepoName(filteredVault);
+
+ const goToFilteredVaultGithubIssues = async () => {
+ if (!filteredVaultRepoName) return;
+
+ const githubLink = `https://github.com/hats-finance/${filteredVaultRepoName}/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 [vaultGithubIssues, setVaultGithubIssues] = useState(undefined);
+ const [isLoadingGH, setIsLoadingGH] = useState(false);
const { data: committeeSubmissions, isLoading, loadingProgress } = useVaultSubmissionsByKeystore();
@@ -82,12 +124,23 @@ export const SubmissionsListPage = () => {
if (titleFilter) {
filteredSubmissions = filteredSubmissions.filter((submission) => {
if (!submission.submissionDataStructure?.title) return false;
- console.log(titleFilter);
return submission.submissionDataStructure.title.toLowerCase().includes(titleFilter.toLowerCase());
});
}
+ if (vaultFilter && vaultFilter !== "all") {
+ filteredSubmissions = filteredSubmissions.filter((submission) => {
+ if (!submission.linkedVault?.id) return false;
+ return submission.linkedVault?.id.toLowerCase() === vaultFilter.toLowerCase();
+ });
+ }
+ if (vaultFilter && vaultFilter !== "all" && onlyShowLabeled && vaultGithubIssues && vaultGithubIssues.length > 0) {
+ filteredSubmissions = filteredSubmissions.filter((submission) => {
+ const ghIssue = getGhIssueFromSubmission(submission, vaultGithubIssues);
+ return ghIssue && ghIssue.validLabels.length > 0;
+ });
+ }
return filteredSubmissions;
- }, [committeeSubmissions, dateFilter, severityFilter, titleFilter]);
+ }, [committeeSubmissions, dateFilter, severityFilter, titleFilter, vaultFilter, onlyShowLabeled, vaultGithubIssues]);
const allSeveritiesOptions = useMemo(() => {
if (!committeeSubmissions) return [];
@@ -103,6 +156,44 @@ export const SubmissionsListPage = () => {
return options;
}, [committeeSubmissions, t]);
+ const allVaultsOptions = useMemo(() => {
+ if (!committeeSubmissions || committeeSubmissions.length === 0) return undefined;
+ const vaults = committeeSubmissions.reduce((prev, submission) => {
+ if (!submission.linkedVault) return prev;
+ const vault = submission.linkedVault;
+ if (vault && !prev.some((v) => v.id === vault.id)) prev.push(vault);
+ return prev;
+ }, []);
+
+ vaults.sort(
+ (a, b) => (b.description?.["project-metadata"].starttime ?? 0) - (a.description?.["project-metadata"].starttime ?? 0)
+ );
+
+ const options: FormSelectInputOption[] =
+ vaults?.map((vault) => ({
+ value: vault.id,
+ label: vault.description?.["project-metadata"].name ?? vault.name,
+ icon: vault.description?.["project-metadata"].icon,
+ onHoverText: `${vault.version} - ${appChains[vault.chainId as number].chain.name}`,
+ helper: (
+
+ {vault.version === "v1"
+ ? `${shortenIfAddress(vault.master.address, { startLength: 6, endLength: 6 })} (PID: ${vault.pid})`
+ : shortenIfAddress(vault.id, { startLength: 6, endLength: 6 })}
+
+
+ ),
+ onHelperClick: () =>
+ window.open(
+ appChains[vault.chainId as number].chain.blockExplorers?.default.url + "/address/" + vault.id ?? vault.master.address,
+ "_blank"
+ ),
+ })) ?? [];
+ options.push({ label: t("all"), value: "all" });
+
+ return options;
+ }, [committeeSubmissions, t]);
+
const createPayoutFromSubmissions = useCreatePayoutFromSubmissions();
const [page, setPage] = useState(1);
@@ -159,6 +250,15 @@ export const SubmissionsListPage = () => {
sessionStorage.setItem(LocalStorage.SelectedSubmissions, JSON.stringify(selectedSubmissions));
}, [selectedSubmissions]);
+ // Set by default first vault as vaultFilter
+ useEffect(() => {
+ if (!allVaultsOptions) return;
+ if (vaultFilter) return;
+
+ if (allVaultsOptions.length === 0) return setVaultFilter("all");
+ setVaultFilter(allVaultsOptions[0].value);
+ }, [allVaultsOptions, vaultFilter]);
+
// Get selected submissions from navigation state
useEffect(() => {
const navigationState = location.state as { selectedSubmissions?: string[] };
@@ -168,33 +268,52 @@ export const SubmissionsListPage = () => {
navigate(location.pathname, { replace: true });
}, [location, navigate]);
- const handleDownloadAsCsv = () => {
- if (!filteredSubmissions) return;
- if (filteredSubmissions.length === 0) return;
-
- const submissionsToDownload =
- selectedSubmissions.length === 0
- ? filteredSubmissions
- : filteredSubmissions.filter((submission) => selectedSubmissions.includes(submission.subId));
-
- const csvString = [
- ["beneficiary", "severity", "title"],
- ...submissionsToDownload.map((submission, idx) => [
- submission.submissionDataStructure?.beneficiary,
- submission.submissionDataStructure?.severity?.toLowerCase(),
- submission.submissionDataStructure?.title.replaceAll(",", "."),
- ]),
- ]
- .map((e) => e.join(","))
- .join("\n");
-
- const blob = new Blob([csvString], { type: "text/csv" });
- const url = window.URL.createObjectURL(blob);
- const a = document.createElement("a");
- a.setAttribute("href", url);
- a.setAttribute("download", `hats-submissions-${new Date().getTime()}.csv`);
- a.click();
- };
+ // Get information from github
+ useEffect(() => {
+ if (!vaultFilter || vaultFilter === "all") return;
+
+ const vault = allVaults?.find((vault) => vault.id.toLowerCase() === vaultFilter.toLowerCase());
+ if (!vault) return;
+ if (vaultGithubIssues !== undefined) return;
+
+ const loadGhIssues = async () => {
+ setIsLoadingGH(true);
+ const ghIssues = await getGithubIssuesFromVault(vault);
+ setVaultGithubIssues(ghIssues);
+ setIsLoadingGH(false);
+ };
+ loadGhIssues();
+
+ console.log(filteredSubmissions);
+ }, [vaultFilter, filteredSubmissions, allVaults, vaultGithubIssues]);
+
+ // const handleDownloadAsCsv = () => {
+ // if (!filteredSubmissions) return;
+ // if (filteredSubmissions.length === 0) return;
+
+ // const submissionsToDownload =
+ // selectedSubmissions.length === 0
+ // ? filteredSubmissions
+ // : filteredSubmissions.filter((submission) => selectedSubmissions.includes(submission.subId));
+
+ // const csvString = [
+ // ["beneficiary", "severity", "title"],
+ // ...submissionsToDownload.map((submission, idx) => [
+ // submission.submissionDataStructure?.beneficiary,
+ // submission.submissionDataStructure?.severity?.toLowerCase(),
+ // submission.submissionDataStructure?.title.replaceAll(",", "."),
+ // ]),
+ // ]
+ // .map((e) => e.join(","))
+ // .join("\n");
+
+ // const blob = new Blob([csvString], { type: "text/csv" });
+ // const url = window.URL.createObjectURL(blob);
+ // const a = document.createElement("a");
+ // a.setAttribute("href", url);
+ // a.setAttribute("download", `hats-submissions-${new Date().getTime()}.csv`);
+ // a.click();
+ // };
const handleChangePage = (direction: number) => () => {
if (direction === -1) {
@@ -278,16 +397,20 @@ export const SubmissionsListPage = () => {
.find((sev) => submission?.submissionDataStructure?.severity?.includes(sev.name.toLowerCase()))
?.name.toLowerCase() ?? submission?.submissionDataStructure?.severity;
+ const ghIssue = getGhIssueFromSubmission(submission, vaultGithubIssues);
+
payoutData = {
...(createNewPayoutData("single") as ISinglePayoutData),
beneficiary: submission?.submissionDataStructure?.beneficiary,
- severity: severity ?? "",
+ severity: ghIssue ? ghIssue?.validLabels[0] ?? "" : severity ?? "",
submissionData: { id: submission?.id, subId: submission?.subId, idx: submission?.submissionIdx },
depositors: getVaultDepositors(vault),
curator: await getVaultCurator(vault),
+ ghIssue,
} as ISinglePayoutData;
} else {
const submissions = committeeSubmissions.filter((sub) => selectedSubmissions.includes(sub.subId));
+
payoutData = {
...(createNewPayoutData("split") as ISplitPayoutData),
beneficiaries: submissions.map((submission) => {
@@ -296,11 +419,14 @@ export const SubmissionsListPage = () => {
.find((sev) => submission?.submissionDataStructure?.severity?.includes(sev.name.toLowerCase()))
?.name.toLowerCase() ?? submission?.submissionDataStructure?.severity;
+ const ghIssue = getGhIssueFromSubmission(submission, vaultGithubIssues);
+
return {
...createNewSplitPayoutBeneficiary(),
beneficiary: submission?.submissionDataStructure?.beneficiary,
- severity: severity ?? "",
+ severity: ghIssue ? ghIssue?.validLabels[0] ?? "" : severity ?? "",
submissionData: { id: submission?.id, subId: submission?.subId, idx: submission?.submissionIdx },
+ ghIssue,
};
}),
usingPointingSystem: (vault.description as IVaultDescriptionV2).usingPointingSystem,
@@ -370,7 +496,7 @@ export const SubmissionsListPage = () => {
-
+
{allPageSelected ? (
) : (
@@ -394,11 +520,41 @@ export const SubmissionsListPage = () => {
colorable
options={allSeveritiesOptions}
noMargin
- onChange={(severity) => setSeverityFilter(severity as string)}
+ onChange={(severity) => {
+ setSeverityFilter(severity as string);
+ setPage(1);
+ }}
/>
+
+
+ {(page - 1) * ITEMS_PER_PAGE + 1}-{(page - 1) * ITEMS_PER_PAGE + quantityInPage}
+ of {filteredSubmissions?.length ?? 0}
+
+
+
+
+ {
+ setVaultFilter(vaultId as string);
+ localStorage.setItem(LocalStorage.SelectedVaultInSubmissions, vaultId as string);
+ setVaultGithubIssues(undefined);
+ setSelectedSubmissions([]);
+ setPage(1);
+ }}
+ />
+
{
placeholder={t("SubmissionsTool.title")}
colorable
noMargin
- onChange={(e) => setTitleFilter(e.target.value as string)}
+ onChange={(e) => {
+ setTitleFilter(e.target.value as string);
+ setPage(1);
+ }}
/>
-
-
-
- {(page - 1) * ITEMS_PER_PAGE + 1}-{(page - 1) * ITEMS_PER_PAGE + quantityInPage}
- of {filteredSubmissions?.length ?? 0}
-
-
-
-
+
+
+
+
+ {vaultFilter !== "all" && vaultGithubIssues && vaultGithubIssues.length > 0 && (
+ setOnlyShowLabeled(e.target.checked)}
+ type="toggle"
+ label={t("onlyShowLabeledIssues")}
+ noMargin
+ />
+ )}
+ {filteredVault && (
+
+ )}
+
@@ -495,6 +667,7 @@ export const SubmissionsListPage = () => {
isChecked={selectedSubmissions.includes(submission.subId)}
key={submission.subId}
submission={submission}
+ ghIssue={getGhIssueFromSubmission(submission, vaultGithubIssues)}
/>
);
})}
@@ -503,24 +676,72 @@ export const SubmissionsListPage = () => {
- {Array.from(
+
+ {(() => {
+ const totalPages = filteredSubmissions ? Math.ceil(filteredSubmissions?.length / ITEMS_PER_PAGE) : 1;
+
+ if (totalPages <= 20) {
+ return Array.from(Array(totalPages).keys()).map((pageIdx) => (
+
setPage(pageIdx + 1)}
+ className={`number ${page === pageIdx + 1 && "current"}`}
+ >
+ {pageIdx + 1}
+
+ ));
+ } else {
+ return (
+ <>
+ {page > 1 && (
+ <>
+
setPage(1)} className={`number ${page === 1 && "current"}`}>
+ 1
+
+
...
+ >
+ )}
+
+ {Array.from(
+ { length: 20 },
+ (_, i) => i + (totalPages - page > 20 ? page : totalPages - 20)
+ ).map((pageIdx) => (
+
setPage(pageIdx)}
+ className={`number ${page === pageIdx && "current"}`}
+ >
+ {pageIdx}
+
+ ))}
+
+ {totalPages - page > 20 &&
...
}
+
setPage(totalPages)} className={`number ${page === totalPages && "current"}`}>
+ {totalPages}
+
+ >
+ );
+ }
+ })()}
+
+ {/* {Array.from(
{ length: filteredSubmissions ? Math.ceil(filteredSubmissions?.length / ITEMS_PER_PAGE) : 1 },
(_, i) => i + 1
).map((pageIdx) => (
setPage(pageIdx)} className={`${page === pageIdx && "current"}`}>
{pageIdx}
- ))}
+ ))} */}
-
*/}
{selectedSubmissions.length >= 1 && (
@@ -576,6 +797,7 @@ export const SubmissionsListPage = () => {
{createPayoutFromSubmissions.isLoading && }
{!vaultsReadyAllChains && }
+ {isLoadingGH && }
);
};
diff --git a/packages/web/src/pages/CommitteeTools/SubmissionsTool/SubmissionsListPage/styles.ts b/packages/web/src/pages/CommitteeTools/SubmissionsTool/SubmissionsListPage/styles.ts
index cfccc17e4..100b392d7 100644
--- a/packages/web/src/pages/CommitteeTools/SubmissionsTool/SubmissionsListPage/styles.ts
+++ b/packages/web/src/pages/CommitteeTools/SubmissionsTool/SubmissionsListPage/styles.ts
@@ -114,6 +114,7 @@ export const StyledSubmissionsListPage = styled.div`
align-items: flex-start;
margin-bottom: ${getSpacing(1)};
gap: ${getSpacing(2)};
+ width: 100%;
.controls-row {
width: 100%;
@@ -125,9 +126,14 @@ export const StyledSubmissionsListPage = styled.div`
width: ${getSpacing(25)};
}
- .title-filter {
+ .title-filter,
+ .vaults-filter {
width: 100%;
}
+
+ .pagination {
+ margin-left: auto;
+ }
}
}
@@ -142,6 +148,10 @@ export const StyledSubmissionsListPage = styled.div`
gap: ${getSpacing(1.5)};
justify-content: center;
+ .number {
+ width: 15px;
+ }
+
p,
.icon {
cursor: pointer;
@@ -172,6 +182,20 @@ export const StyledSubmissionsListPage = styled.div`
padding: ${getSpacing(0)} ${getSpacing(3)} ${getSpacing(4)};
transform: translateX(-${getSpacing(3)});
}
+
+ .flex {
+ width: 100%;
+ display: flex;
+ align-items: center;
+ gap: ${getSpacing(3)};
+
+ .flex-end {
+ display: flex;
+ align-items: center;
+ gap: ${getSpacing(2)};
+ margin-left: auto;
+ }
+ }
`;
export const StyledSubmissionCard = styled.div<{
@@ -253,6 +277,17 @@ export const StyledSubmissionCard = styled.div<{
flex-direction: column;
gap: ${getSpacing(0.5)};
+ .severity {
+ display: flex;
+ gap: ${getSpacing(1)};
+ align-items: center;
+
+ span {
+ font-size: var(--xxsmall);
+ font-weight: 700;
+ }
+ }
+
.submission-title {
font-weight: 700;
padding-left: ${getSpacing(2)};
diff --git a/packages/web/src/pages/CommitteeTools/SubmissionsTool/submissionsService.api.ts b/packages/web/src/pages/CommitteeTools/SubmissionsTool/submissionsService.api.ts
index 1f38faf0a..c163fe1a6 100644
--- a/packages/web/src/pages/CommitteeTools/SubmissionsTool/submissionsService.api.ts
+++ b/packages/web/src/pages/CommitteeTools/SubmissionsTool/submissionsService.api.ts
@@ -1,6 +1,14 @@
-import { IPayoutData, ISubmittedSubmission, IVault, IVaultInfo, PayoutType } from "@hats.finance/shared";
+import {
+ GithubIssue,
+ IPayoutData,
+ ISubmittedSubmission,
+ IVault,
+ IVaultInfo,
+ PayoutType,
+ severitiesOrder,
+} from "@hats.finance/shared";
import { axiosClient } from "config/axiosClient";
-import { BASE_SERVICE_URL } from "settings";
+import { BASE_SERVICE_URL, HATS_GITHUB_BOT_ID } from "settings";
export const extractSubmissionData = (
submission: ISubmittedSubmission,
@@ -202,3 +210,41 @@ export async function createPayoutFromSubmissions(
});
return res.data.upsertedId;
}
+
+export async function getGithubIssuesFromVault(vault: IVault): Promise {
+ const extractTxHashFromBody = (issue: GithubIssue): any => {
+ // const txHash = issue.body.match(/(0x[a-fA-F0-9]{64})/)?.[0];
+ const txHash = issue.body.match(/(\*\*Submission hash \(on-chain\):\*\* (.*)\n)/)?.[2] ?? undefined;
+ return txHash;
+ };
+
+ const mapGithubIssue = (issue: any): GithubIssue => {
+ return {
+ id: issue.id,
+ number: issue.number,
+ title: issue.title,
+ createdBy: issue.user.id,
+ labels: issue.labels.map((label: any) => label.name),
+ validLabels: issue.labels
+ .filter((label: any) => severitiesOrder.includes((label.name as string).toLowerCase()))
+ .map((label: any) => (label.name as string).toLowerCase()),
+ createdAt: issue.created_at,
+ body: issue.body,
+ txHash: extractTxHashFromBody(issue),
+ };
+ };
+
+ const res = await axiosClient.get(`${BASE_SERVICE_URL}/github-repos/gh-issues/${vault.id}`);
+ const issues = res.data.githubIssues.map(mapGithubIssue) as GithubIssue[];
+
+ return issues.filter((issue) => issue.createdBy === HATS_GITHUB_BOT_ID) ?? [];
+}
+
+export function getGhIssueFromSubmission(submission?: ISubmittedSubmission, ghIssues?: GithubIssue[]): GithubIssue | undefined {
+ if (!ghIssues || !submission) return undefined;
+
+ const sameTxHash = ghIssues.filter((issue) => issue.txHash === submission.txid);
+ const sameTitle = sameTxHash.filter((issue) => issue.title === submission.submissionDataStructure?.title);
+
+ return sameTitle[0];
+}
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 6597bb653..9fd1ee8bd 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
@@ -21,6 +21,7 @@ import { useVaultRepoName } from "pages/Honeypots/VaultDetailsPage/hooks";
import { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { shortenIfAddress } from "utils/addresses.utils";
+import { getForkedRepoName } from "utils/slug.utils";
import { checkUrl } from "utils/yup.utils";
import { StyledContractsList, StyledInScopeSection } from "./styles";
@@ -69,8 +70,7 @@ export const InScopeSection = ({ vault }: InScopeSectionProps) => {
return vault.description.scope?.reposInformation ?? [];
}
- const vaultName = vault.description["project-metadata"].name.replace(/[^\w\s]| /gi, "-");
- const forkedRepoName = `${vaultName}-${vault.id}`;
+ const forkedRepoName = getForkedRepoName(vault);
const forkedRepoUrl = `https://github.com/hats-finance/${forkedRepoName}`;
const reposInformation: IVaultRepoInformation[] = [
diff --git a/packages/web/src/pages/VaultEditor/VaultEditorFormPage/SetupSteps/shared/VaultCuratorForm/VaultCuratorForm.tsx b/packages/web/src/pages/VaultEditor/VaultEditorFormPage/SetupSteps/shared/VaultCuratorForm/VaultCuratorForm.tsx
index 338eb4d8a..27d772421 100644
--- a/packages/web/src/pages/VaultEditor/VaultEditorFormPage/SetupSteps/shared/VaultCuratorForm/VaultCuratorForm.tsx
+++ b/packages/web/src/pages/VaultEditor/VaultEditorFormPage/SetupSteps/shared/VaultCuratorForm/VaultCuratorForm.tsx
@@ -56,6 +56,7 @@ export const VaultCuratorForm = () => {
colorable
disabled={allFormDisabled || isLoadingCurators}
options={curatorItems}
+ helper={curators?.find((c) => c.username === selectedCurator)?.addresses?.[0]}
{...field}
value={field.value ?? ""}
flexExpand
diff --git a/packages/web/src/settings.ts b/packages/web/src/settings.ts
index 8a75a7f36..84398b440 100644
--- a/packages/web/src/settings.ts
+++ b/packages/web/src/settings.ts
@@ -25,3 +25,5 @@ export const HATS_STAKING_VAULT = {
address: "0x1025b2248cb6aeaf93c7e4d10b19f90f5b4ea090",
chain: arbitrum,
};
+
+export const HATS_GITHUB_BOT_ID = 132391680;
diff --git a/packages/web/src/utils/slug.utils.ts b/packages/web/src/utils/slug.utils.ts
index 8884fb2ff..739cd4608 100644
--- a/packages/web/src/utils/slug.utils.ts
+++ b/packages/web/src/utils/slug.utils.ts
@@ -1,3 +1,5 @@
+import { IVault } from "@hats.finance/shared";
+
export const slugify = (str: string) =>
str
.toLowerCase()
@@ -5,3 +7,13 @@ export const slugify = (str: string) =>
.replace(/[^\w\s-]/g, "")
.replace(/[\s_-]+/g, "-")
.replace(/^-+|-+$/g, "");
+
+export const getForkedRepoName = (vault: IVault) => {
+ const vaultName = vault.description?.["project-metadata"].name;
+ if (!vaultName) return "";
+
+ const vaultSlug = vaultName.replace(/[^\w\s]| /gi, "-");
+ const forkedRepoName = `${vaultSlug}-${vault.id}`;
+
+ return forkedRepoName;
+};