diff --git a/packages/shared/package.json b/packages/shared/package.json index be7ceba1b..3d8552b30 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -1,6 +1,6 @@ { "name": "@hats.finance/shared", - "version": "1.1.124", + "version": "1.1.125", "description": "", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/shared/src/types/editor.ts b/packages/shared/src/types/editor.ts index ce2d0bdc1..b654d3c00 100644 --- a/packages/shared/src/types/editor.ts +++ b/packages/shared/src/types/editor.ts @@ -88,6 +88,7 @@ export interface IBaseEditedVaultDescription { emails: IEditedCommunicationEmail[]; oneLiner?: string; intendedCompetitionAmount?: number; + loc?: number; curator?: { username: string; role: CuratorRole; diff --git a/packages/shared/src/types/payout.ts b/packages/shared/src/types/payout.ts index 8a9335b84..7bd1855bf 100644 --- a/packages/shared/src/types/payout.ts +++ b/packages/shared/src/types/payout.ts @@ -49,13 +49,28 @@ export type GithubIssue = { createdBy: number; labels: string[]; validLabels: string[]; + createdAt: string; + body: string; + txHash?: string; + severity?: string; bonusPointsLabels: { needsFix: boolean; needsTest: boolean; }; +}; + +export type GithubPR = { + id: number; + number: number; + title: string; + createdBy: number; + labels: string[]; createdAt: string; body: string; txHash?: string; + bonusSubmissionStatus: "COMPLETE" | "INCOMPLETE" | "PENDING"; + linkedIssueNumber?: number; + linkedIssue?: GithubIssue; }; export type IPayoutData = ISinglePayoutData | ISplitPayoutData; @@ -80,7 +95,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; + ghIssue?: GithubIssue | GithubPR; } // Only for v2 vaults @@ -106,7 +121,7 @@ export interface ISplitPayoutBeneficiary { nftUrl: string; submissionData?: { id: string; subId: string; idx: number }; decryptedSubmission?: Omit; // Omit: workaround to avoid circular dependency; - ghIssue?: GithubIssue; + ghIssue?: GithubIssue | GithubPR; } export interface IPayoutSignature { diff --git a/packages/shared/src/types/types.ts b/packages/shared/src/types/types.ts index 73b437641..1413c83f1 100644 --- a/packages/shared/src/types/types.ts +++ b/packages/shared/src/types/types.ts @@ -148,6 +148,7 @@ interface IBaseVaultDescription { starttime?: number; oneLiner?: string; intendedCompetitionAmount?: number; + loc?: number; curator?: { username: string; role: CuratorRole; diff --git a/packages/shared/src/utils/vaults.utils.ts b/packages/shared/src/utils/vaults.utils.ts index e5e9d0471..129f44aa4 100644 --- a/packages/shared/src/utils/vaults.utils.ts +++ b/packages/shared/src/utils/vaults.utils.ts @@ -175,7 +175,15 @@ export const getVaultInfoWithCommittee = async ( export const getAllVaultsAddressesByChain = async ( chainId: number -): Promise<{ id: string; registered: boolean; version: IVault["version"]; claimsManager: IVault["claimsManager"] }[]> => { +): Promise< + { + id: string; + descriptionHash: string; + registered: boolean; + version: IVault["version"]; + claimsManager: IVault["claimsManager"]; + }[] +> => { if (!chainId) return []; try { @@ -183,6 +191,7 @@ export const getAllVaultsAddressesByChain = async ( query getVaults { vaults(where: {version_not: "v1"}) { id + descriptionHash registered claimsManager version diff --git a/packages/web/src/components/Seo/Seo.tsx b/packages/web/src/components/Seo/Seo.tsx index 1f7201ec7..524af374e 100644 --- a/packages/web/src/components/Seo/Seo.tsx +++ b/packages/web/src/components/Seo/Seo.tsx @@ -17,20 +17,6 @@ export const Seo = ({ isMainPage, title, description, image }: SeoProps) => { image: image ?? require("../../assets/images/hats_og.png"), }; - // const allowedEndpoints = [ - // "'self'", - // "https://*.hats.finance", - // "https://*.infura.io", - // "https://api.coingecko.com/", - // "https://cloudflare-eth.com/", - // new URL(stagingServiceUrl).protocol + "//" + new URL(stagingServiceUrl).host, - // new URL(prodServiceUrl).protocol + "//" + new URL(prodServiceUrl).host, - // ...Object.values(appChains).map((chain) => chain.subgraph), - // ...Object.values(appChains).map((chain) => chain.uniswapSubgraph), - // ...Object.values(externalPricingProvidersUrls), - // ]; - // const connectSrc = allowedEndpoints.join(" "); - return ( {isMainPage && ( diff --git a/packages/web/src/hooks/nft/getStakerData.ts b/packages/web/src/hooks/nft/getStakerData.ts deleted file mode 100644 index c4b73a3ff..000000000 --- a/packages/web/src/hooks/nft/getStakerData.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { GET_STAKER } from "graphql/subgraph"; -import { appChains } from "settings"; -import { IStaker } from "types"; - -/** - * Query the vaults subgrapt for staker data, we can know in which vaults a specific address has - * staked. so we can then check eligibility for nft minting - * @param chainId - * @param address - * @returns - */ -export async function getStakerData(chainId: number, address: string) { - const subgraphUrl = appChains[chainId].subgraph; - const res = await fetch(subgraphUrl, { - method: "POST", - body: JSON.stringify({ query: GET_STAKER, variables: { address } }), - headers: { "Content-Type": "application/json" }, - cache: "default", - }); - const dataJson = (await res.json()) as { data: { stakers: IStaker[] } }; - return dataJson.data.stakers; -} diff --git a/packages/web/src/hooks/subgraph/vaults/useMultiChainVaults.ts b/packages/web/src/hooks/subgraph/vaults/useMultiChainVaults.ts index a181e0064..17f455028 100644 --- a/packages/web/src/hooks/subgraph/vaults/useMultiChainVaults.ts +++ b/packages/web/src/hooks/subgraph/vaults/useMultiChainVaults.ts @@ -6,8 +6,6 @@ import { useAccount } from "wagmi"; import { parseMasters, parsePayouts, parseUserNfts, parseVaults } from "./parser"; import { IGraphVaultsData, getSubgraphData } from "./vaultsService"; -const DATA_REFRESH_TIME = 30000; - const INITIAL_NETWORK_DATA = { vaults: [] as IVault[], masters: [] as IMaster[], @@ -29,8 +27,7 @@ export const useMultiChainVaultsV2 = () => { queries: Object.keys(appChains).map((chainId) => ({ queryKey: ["subgraph", chainId], queryFn: () => getSubgraphData(+chainId, account), - // refetchInterval: DATA_REFRESH_TIME, // We dont want to refetch data automatically anymore - refetchIntervalInBackground: false, + refetchOnWindowFocus: false, retry: false, })), }); diff --git a/packages/web/src/languages/en.json b/packages/web/src/languages/en.json index 8de73b919..1f86a0b80 100644 --- a/packages/web/src/languages/en.json +++ b/packages/web/src/languages/en.json @@ -749,6 +749,9 @@ "bonusPointsReminder": "Remember, you have 12 hours to submit after claiming. Happy fixing and testing!", "bonusPointsEnabled": "Enable bonus points?", "claimFixAndTest": "Claim fix and test", + "linesOfCode": "Lines of code (LoC)", + "issueAlreadyHaveValidSubmission": "This issue already have a valid submission. You can submit complementary submission in other issue.", + "oneSubmissionIsBeingReviewed": "One submission is being reviewed. Please wait, if the submission is not complete, you can submit another one.", "MyWallet": { "overview": "Overview", "pointValue": "Point value", @@ -1650,7 +1653,9 @@ "curator-placeholder": "Select curator of the vault", "curatorRole": "Curator role", "curatorRole-placeholder": "Select the role of the curator", - "clearCurator": "Clear curator" + "clearCurator": "Clear curator", + "loc-placeholder": "Enter number of lines of code", + "loc": "Lines of code" }, "signatureMessage": "I hereby confirm the details in ipfs hash {{ipfsHash}}.", "committee-details": "Committee Details", diff --git a/packages/web/src/pages/CommitteeTools/PayoutsTool/PayoutFormPage/PayoutFormPage.tsx b/packages/web/src/pages/CommitteeTools/PayoutsTool/PayoutFormPage/PayoutFormPage.tsx index 007040b28..0cc8cec59 100644 --- a/packages/web/src/pages/CommitteeTools/PayoutsTool/PayoutFormPage/PayoutFormPage.tsx +++ b/packages/web/src/pages/CommitteeTools/PayoutsTool/PayoutFormPage/PayoutFormPage.tsx @@ -114,6 +114,13 @@ export const PayoutFormPage = () => { }); } } else { + if (vault.description["project-metadata"].bonusPointsEnabled) { + severities.push({ + label: "Complementary", + value: "complementary", + }); + } + for (const splitPayoutBeneficiary of payout.payoutData.beneficiaries) { if ( splitPayoutBeneficiary.severity && 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 7de4e70e1..aec422109 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,4 +1,4 @@ -import { GithubIssue, IPayoutResponse, ISplitPayoutData, IVault } from "@hats.finance/shared"; +import { GithubIssue, GithubPR, 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"; @@ -8,7 +8,12 @@ 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 { + getGhIssueFromSubmission, + getGhPRFromSubmission, + getGithubIssuesFromVault, + getGithubPRsFromVault, +} from "pages/CommitteeTools/SubmissionsTool/submissionsService.api"; import { useVaultSubmissionsByKeystore } from "pages/CommitteeTools/SubmissionsTool/submissionsService.hooks"; import { useEffect, useState } from "react"; import { Controller, UseFieldArrayRemove, useWatch } from "react-hook-form"; @@ -99,6 +104,7 @@ export const SplitPayoutBeneficiaryForm = ({ }); const [vaultGithubIssues, setVaultGithubIssues] = useState(undefined); + const [vaultGithubPRs, setVaultGithubPRs] = useState(undefined); const [isLoadingGH, setIsLoadingGH] = useState(false); // Get information from github @@ -113,6 +119,12 @@ export const SplitPayoutBeneficiaryForm = ({ setIsLoadingGH(false); }; loadGhIssues(); + + const loadGhPRs = async () => { + const ghPRs = await getGithubPRsFromVault(vault); + setVaultGithubPRs(ghPRs); + }; + loadGhPRs(); }, [vault, vaultGithubIssues, beneficiarySubmission, isLoadingGH]); const getMoreOptions = () => { @@ -153,6 +165,10 @@ export const SplitPayoutBeneficiaryForm = ({ ]; }; + const selectedSubmission = isPayoutCreated + ? beneficiaries[index]?.decryptedSubmission ?? beneficiarySubmission! + : beneficiarySubmission!; + return (
{index + 1}.
@@ -166,10 +182,10 @@ export const SplitPayoutBeneficiaryForm = ({ submission={ isPayoutCreated ? beneficiaries[index]?.decryptedSubmission ?? beneficiarySubmission! : beneficiarySubmission! } - ghIssue={getGhIssueFromSubmission( - isPayoutCreated ? beneficiaries[index]?.decryptedSubmission ?? beneficiarySubmission! : beneficiarySubmission!, - vaultGithubIssues - )} + ghIssue={ + getGhIssueFromSubmission(selectedSubmission, vaultGithubIssues) || + getGhPRFromSubmission(selectedSubmission, vaultGithubPRs, vaultGithubIssues) + } />
) : ( diff --git a/packages/web/src/pages/CommitteeTools/PayoutsTool/utils/autocalculateMultiPayout.ts b/packages/web/src/pages/CommitteeTools/PayoutsTool/utils/autocalculateMultiPayout.ts index 00a12958e..9316667b6 100644 --- a/packages/web/src/pages/CommitteeTools/PayoutsTool/utils/autocalculateMultiPayout.ts +++ b/packages/web/src/pages/CommitteeTools/PayoutsTool/utils/autocalculateMultiPayout.ts @@ -1,6 +1,11 @@ -import { ISplitPayoutBeneficiary, ISplitPayoutData } from "@hats.finance/shared"; +import { GithubPR, ISplitPayoutBeneficiary, ISplitPayoutData } from "@hats.finance/shared"; import millify from "millify"; +const BONUS_POINTS_CONSTRAINTS = { + fix: 0.1, // 10% + test: 0.05, // 5% +}; + const DECIMALS_TO_USE = 4; // type IMultipayoutCalculation = ISplitPayoutBeneficiary[]; @@ -98,16 +103,39 @@ export const autocalculateMultiPayoutPointingSystem = ( const needPoints = beneficiaries.every((ben) => ben.percentageOfPayout === "" || ben.percentageOfPayout === undefined); for (let beneficiary of beneficiaries) { - const sevInfo = constraints.find((constraint) => constraint.severity.toLowerCase() === beneficiary.severity.toLowerCase()); - const defaultPoints = sevInfo?.points ? `${sevInfo.points.value.first}` : "1"; - - const beneficiaryCalculated: IBeneficiaryWithCalcs = { - ...beneficiary, - percentageOfPayout: needPoints ? defaultPoints : beneficiary.percentageOfPayout, - amount: 0, - calculatedReward: 0, - }; - beneficiariesCalculated.push(beneficiaryCalculated); + if (beneficiary.severity.toLowerCase() === "complementary") { + const mainIssueSev = (beneficiary.ghIssue as GithubPR).linkedIssue?.severity; + const mainIssueSevInfo = constraints.find( + (constraint) => constraint.severity.toLowerCase() === mainIssueSev?.toLowerCase() + ); + const mainIssuePoints = mainIssueSevInfo?.points ? `${mainIssueSevInfo.points.value.first}` : "1"; + + let totalMultiplier = 0; + + if (beneficiary.ghIssue?.labels?.includes("complete-fix")) totalMultiplier += BONUS_POINTS_CONSTRAINTS.fix; + if (beneficiary.ghIssue?.labels?.includes("complete-test")) totalMultiplier += BONUS_POINTS_CONSTRAINTS.test; + + const complementaryPoints = totalMultiplier * +mainIssuePoints; + + const beneficiaryCalculated: IBeneficiaryWithCalcs = { + ...beneficiary, + percentageOfPayout: needPoints ? `${complementaryPoints.toFixed(4)}` : beneficiary.percentageOfPayout, + amount: 0, + calculatedReward: 0, + }; + beneficiariesCalculated.push(beneficiaryCalculated); + } else { + const sevInfo = constraints.find((constraint) => constraint.severity.toLowerCase() === beneficiary.severity.toLowerCase()); + const defaultPoints = sevInfo?.points ? `${sevInfo.points.value.first}` : "1"; + + const beneficiaryCalculated: IBeneficiaryWithCalcs = { + ...beneficiary, + percentageOfPayout: needPoints ? defaultPoints : beneficiary.percentageOfPayout, + amount: 0, + calculatedReward: 0, + }; + beneficiariesCalculated.push(beneficiaryCalculated); + } } const totalPointsToPay = beneficiariesCalculated.reduce((prev, curr) => prev + +curr.percentageOfPayout, 0); diff --git a/packages/web/src/pages/CommitteeTools/SubmissionsTool/SubmissionsListPage/SubmissionCard.tsx b/packages/web/src/pages/CommitteeTools/SubmissionsTool/SubmissionsListPage/SubmissionCard.tsx index 2bd4a89d2..895503b80 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 { GithubIssue, ISubmittedSubmission, IVulnerabilitySeverity, parseSeverityName } from "@hats.finance/shared"; +import { GithubIssue, GithubPR, 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,7 +18,7 @@ type SubmissionCardProps = { inPayout?: boolean; isChecked?: boolean; onCheckChange?: (submission: ISubmittedSubmission) => void; - ghIssue?: GithubIssue; + ghIssue?: GithubIssue | GithubPR; }; export const SubmissionCard = ({ @@ -37,6 +37,12 @@ export const SubmissionCard = ({ const commChannel = submissionData?.communicationChannel; const severityColors = getSeveritiesColorsArray(vault); + const isGithubPR = (issue: GithubIssue | GithubPR): issue is GithubPR => { + return "linkedIssueNumber" in issue; + }; + + const isComplementary = ghIssue ? isGithubPR(ghIssue) && !!ghIssue.linkedIssueNumber : false; + const createdAt = new Date(+submission?.createdAt * 1000); const severityIndex = @@ -67,25 +73,55 @@ export const SubmissionCard = ({
{submissionData?.severity && ( - {ghIssue && ( + {ghIssue && !isComplementary && ( {t("ghIssue")} #{ghIssue.number} - )} - {t("submittedAs")}: + {!isComplementary && {t("submittedAs")}:} - {ghIssue && ghIssue?.validLabels.length > 0 && ( + {ghIssue && isComplementary ? ( + (() => { + const getText = () => { + const labels = (ghIssue as GithubPR).labels; + if (labels.includes("complete-fix") && labels.includes("complete-test")) { + return `COMPLETE (fix & test) -> ${(ghIssue as GithubPR).linkedIssue?.severity}`; + } else if (labels.includes("complete-fix")) { + return `COMPLETE (fix) -> ${(ghIssue as GithubPR).linkedIssue?.severity}`; + } else if (labels.includes("complete-test")) { + return `COMPLETE (test) -> ${(ghIssue as GithubPR).linkedIssue?.severity}`; + } + + return (ghIssue as GithubPR).bonusSubmissionStatus; + }; + + return ( + <> + {t("status")}: + + + ); + })() + ) : ( <> - {t("labeledAs")}: - + {ghIssue && (ghIssue as GithubIssue)?.validLabels?.length > 0 && ( + <> + {t("labeledAs")}: + + + )} )} diff --git a/packages/web/src/pages/CommitteeTools/SubmissionsTool/SubmissionsListPage/SubmissionsListPage.tsx b/packages/web/src/pages/CommitteeTools/SubmissionsTool/SubmissionsListPage/SubmissionsListPage.tsx index f30e8a023..5e45e9b24 100644 --- a/packages/web/src/pages/CommitteeTools/SubmissionsTool/SubmissionsListPage/SubmissionsListPage.tsx +++ b/packages/web/src/pages/CommitteeTools/SubmissionsTool/SubmissionsListPage/SubmissionsListPage.tsx @@ -1,5 +1,6 @@ import { GithubIssue, + GithubPR, IPayoutData, ISinglePayoutData, ISplitPayoutData, @@ -56,7 +57,12 @@ 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 { + getGhIssueFromSubmission, + getGhPRFromSubmission, + getGithubIssuesFromVault, + getGithubPRsFromVault, +} from "../submissionsService.api"; import { useCreatePayoutFromSubmissions, useVaultSubmissionsByKeystore } from "../submissionsService.hooks"; import { SubmissionCard } from "./SubmissionCard"; import { StyledSubmissionsListPage } from "./styles"; @@ -102,6 +108,7 @@ export const SubmissionsListPage = () => { }; const [vaultGithubIssues, setVaultGithubIssues] = useState(undefined); + const [vaultGithubPRs, setVaultGithubPRs] = useState(undefined); const [isLoadingGH, setIsLoadingGH] = useState(false); const { data: committeeSubmissions, isLoading, loadingProgress } = useVaultSubmissionsByKeystore(); @@ -136,11 +143,21 @@ export const SubmissionsListPage = () => { if (vaultFilter && vaultFilter !== "all" && onlyShowLabeled && vaultGithubIssues && vaultGithubIssues.length > 0) { filteredSubmissions = filteredSubmissions.filter((submission) => { const ghIssue = getGhIssueFromSubmission(submission, vaultGithubIssues); - return ghIssue && ghIssue.validLabels.length > 0; + const ghPR = getGhPRFromSubmission(submission, vaultGithubPRs); + return (ghIssue && ghIssue.validLabels.length > 0) || (ghPR && ghPR.bonusSubmissionStatus === "COMPLETE"); }); } return filteredSubmissions; - }, [committeeSubmissions, dateFilter, severityFilter, titleFilter, vaultFilter, onlyShowLabeled, vaultGithubIssues]); + }, [ + committeeSubmissions, + dateFilter, + severityFilter, + titleFilter, + vaultFilter, + onlyShowLabeled, + vaultGithubIssues, + vaultGithubPRs, + ]); const allSeveritiesOptions = useMemo(() => { if (!committeeSubmissions) return []; @@ -284,7 +301,11 @@ export const SubmissionsListPage = () => { }; loadGhIssues(); - console.log(filteredSubmissions); + const loadGhPRs = async () => { + const ghPRs = await getGithubPRsFromVault(vault); + setVaultGithubPRs(ghPRs); + }; + loadGhPRs(); }, [vaultFilter, filteredSubmissions, allVaults, vaultGithubIssues, isLoadingGH]); // const handleDownloadAsCsv = () => { @@ -398,6 +419,7 @@ export const SubmissionsListPage = () => { ?.name.toLowerCase() ?? submission?.submissionDataStructure?.severity; const ghIssue = getGhIssueFromSubmission(submission, vaultGithubIssues); + const ghPR = getGhPRFromSubmission(submission, vaultGithubPRs, vaultGithubIssues); payoutData = { ...(createNewPayoutData("single") as ISinglePayoutData), @@ -406,7 +428,7 @@ export const SubmissionsListPage = () => { submissionData: { id: submission?.id, subId: submission?.subId, idx: submission?.submissionIdx }, depositors: getVaultDepositors(vault), curator: await getVaultCurator(vault), - ghIssue, + ghIssue: ghIssue || ghPR, } as ISinglePayoutData; } else { const submissions = committeeSubmissions.filter((sub) => selectedSubmissions.includes(sub.subId)); @@ -420,13 +442,14 @@ export const SubmissionsListPage = () => { ?.name.toLowerCase() ?? submission?.submissionDataStructure?.severity; const ghIssue = getGhIssueFromSubmission(submission, vaultGithubIssues); + const ghPR = getGhPRFromSubmission(submission, vaultGithubPRs, vaultGithubIssues); return { ...createNewSplitPayoutBeneficiary(), beneficiary: submission?.submissionDataStructure?.beneficiary, severity: ghIssue ? ghIssue?.validLabels[0] ?? "" : severity ?? "", submissionData: { id: submission?.id, subId: submission?.subId, idx: submission?.submissionIdx }, - ghIssue, + ghIssue: ghIssue || ghPR, }; }), usingPointingSystem: (vault.description as IVaultDescriptionV2).usingPointingSystem, @@ -435,6 +458,8 @@ export const SubmissionsListPage = () => { } as ISplitPayoutData; } + console.log(payoutData); + try { const payoutId = await createPayoutFromSubmissions.mutateAsync({ vaultInfo, type: payoutType, payoutData }); setSelectedSubmissions([]); @@ -550,6 +575,7 @@ export const SubmissionsListPage = () => { setVaultFilter(vaultId as string); localStorage.setItem(LocalStorage.SelectedVaultInSubmissions, vaultId as string); setVaultGithubIssues(undefined); + setVaultGithubPRs(undefined); setSelectedSubmissions([]); setPage(1); }} @@ -667,7 +693,10 @@ export const SubmissionsListPage = () => { isChecked={selectedSubmissions.includes(submission.subId)} key={submission.subId} submission={submission} - ghIssue={getGhIssueFromSubmission(submission, vaultGithubIssues)} + ghIssue={ + getGhIssueFromSubmission(submission, vaultGithubIssues) || + getGhPRFromSubmission(submission, vaultGithubPRs, vaultGithubIssues) + } /> ); })} diff --git a/packages/web/src/pages/CommitteeTools/SubmissionsTool/submissionsService.api.ts b/packages/web/src/pages/CommitteeTools/SubmissionsTool/submissionsService.api.ts index 0d1930180..04f42cd61 100644 --- a/packages/web/src/pages/CommitteeTools/SubmissionsTool/submissionsService.api.ts +++ b/packages/web/src/pages/CommitteeTools/SubmissionsTool/submissionsService.api.ts @@ -1,5 +1,6 @@ import { GithubIssue, + GithubPR, IPayoutData, ISubmittedSubmission, IVault, @@ -139,7 +140,9 @@ export const extractSubmissionData = ( return { beneficiary: beneficiary ?? "--", - severity: firstLine?.slice(firstLine?.lastIndexOf("(") + 1, -1), + severity: firstLine?.startsWith("COMPLEMENTARY") + ? "complementary" + : firstLine?.slice(firstLine?.lastIndexOf("(") + 1, -1), title: firstLine?.slice(0, firstLine.lastIndexOf("(") - 1) ?? "--", content: messageToUse ?? "--", githubUsername: githubUsername ?? "--", @@ -211,6 +214,49 @@ export async function createPayoutFromSubmissions( return res.data.upsertedId; } +export async function getGithubPRsFromVault(vault: IVault): Promise { + if (!vault) return []; + + const extractLinkedIssueNumberFromTitle = (pr: GithubPR): number | undefined => { + // const txHash = issue.body.match(/(0x[a-fA-F0-9]{64})/)?.[0]; + const issueNumber = pr.title.match(/#(\d+)/)?.[1] ?? undefined; + return issueNumber ? parseInt(issueNumber) : undefined; + }; + + const extractTxHashFromBody = (pr: GithubPR): any => { + // const txHash = issue.body.match(/(0x[a-fA-F0-9]{64})/)?.[0]; + const txHash = pr.body.match(/(\*\*Submission hash \(on-chain\):\*\* (.*)\n)/)?.[2] ?? undefined; + return txHash; + }; + + const mapGithubPRs = (pr: any): GithubPR => { + return { + id: pr.id, + number: pr.number, + title: pr.title, + createdBy: pr.user.id, + labels: pr.labels.map((label: any) => label.name), + createdAt: pr.created_at, + body: pr.body, + txHash: extractTxHashFromBody(pr), + bonusSubmissionStatus: pr.labels.some( + (label: any) => + (label.name as string).toLowerCase() === "complete-fix" || (label.name as string).toLowerCase() === "complete-test" + ) + ? "COMPLETE" + : pr.labels.some((label: any) => (label.name as string).toLowerCase() === "incomplete") + ? "INCOMPLETE" + : "PENDING", + linkedIssueNumber: extractLinkedIssueNumberFromTitle(pr), + }; + }; + + const res = await axiosClient.get(`${BASE_SERVICE_URL}/github-repos/gh-prs/${vault.id}`); + const issues = res.data.githubPRs.map(mapGithubPRs) as GithubPR[]; + + return issues.filter((issue) => issue.createdBy === HATS_GITHUB_BOT_ID) ?? []; +} + export async function getGithubIssuesFromVault(vault: IVault): Promise { if (!vault) return []; @@ -254,3 +300,27 @@ export function getGhIssueFromSubmission(submission?: ISubmittedSubmission, ghIs return sameTitle[0]; } + +export function getGhPRFromSubmission( + submission?: ISubmittedSubmission, + ghPRs?: GithubPR[], + ghIssues?: GithubIssue[] +): GithubPR | undefined { + if (!ghPRs || !submission) return undefined; + + const sameTxHash = ghPRs.filter((pr) => pr.txHash === submission.txid); + const sameTitle = sameTxHash.filter((pr) => + submission.submissionDataStructure?.title.startsWith(`COMPLEMENTARY [Issue #${pr.linkedIssueNumber}]`) + ); + + const prFound = sameTitle[0]; + const linkedIssue = ghIssues?.find((issue) => issue.number === prFound?.linkedIssueNumber); + + return { + ...prFound, + linkedIssue: { + ...(linkedIssue as GithubIssue), + severity: linkedIssue?.validLabels[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 acd44f81d..bf1c324f6 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 @@ -10,6 +10,7 @@ import ArrowForwardIcon from "@mui/icons-material/ArrowForward"; import DocumentIcon from "@mui/icons-material/DescriptionOutlined"; import DiffIcon from "@mui/icons-material/DifferenceOutlined"; import OpenIcon from "@mui/icons-material/LaunchOutlined"; +import HashtagIcon from "@mui/icons-material/NumbersOutlined"; import OverviewIcon from "@mui/icons-material/SelfImprovementOutlined"; import TerminalIcon from "@mui/icons-material/Terminal"; import ContractsIcon from "@mui/icons-material/ViewInAr"; @@ -215,6 +216,17 @@ export const InScopeSection = ({ vault }: InScopeSectionProps) => { )} + {/* LoC */} + {vault.description["project-metadata"].loc && ( + <> +

+ + {t("linesOfCode")}: + {vault.description["project-metadata"].loc} +

+ + )} + {/* Overview */} {vault.description.scope?.description && ( <> 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 e1972df24..3631c0c76 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,4 +1,4 @@ -import { GithubIssue, IVault, allowedElementsMarkdown, parseSeverityName } from "@hats.finance/shared"; +import { GithubIssue, GithubPR, IVault, allowedElementsMarkdown, parseSeverityName } from "@hats.finance/shared"; import MDEditor from "@uiw/react-md-editor"; import { Pill } from "components"; import moment from "moment"; @@ -10,9 +10,10 @@ import { StyledPublicSubmissionCard } from "./styles"; type PublicSubmissionCardProps = { vault: IVault; submission: GithubIssue; + linkedPRs?: GithubPR[]; }; -function PublicSubmissionCard({ vault, submission }: PublicSubmissionCardProps) { +function PublicSubmissionCard({ vault, submission, linkedPRs = [] }: PublicSubmissionCardProps) { const { t } = useTranslation(); const [isOpen, setIsOpen] = useState(false); @@ -43,7 +44,7 @@ function PublicSubmissionCard({ vault, submission }: PublicSubmissionCardProps) {showExtraInfo && bonusPointsEnabled && (submission.bonusPointsLabels.needsFix || submission.bonusPointsLabels.needsTest) && ( - + )}
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 index 6a4ac265b..aba757271 100644 --- 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 @@ -1,4 +1,4 @@ -import { GithubIssue, IClaimedIssue } from "@hats.finance/shared"; +import { GithubIssue, GithubPR, 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"; @@ -26,9 +26,10 @@ export const getClaimedBy = (claimedIssue: IClaimedIssue | undefined) => { type SplitPointsActionsProps = { vault: IVault; submission: GithubIssue; + linkedPRs: GithubPR[]; }; -export const SplitPointsActions = ({ vault, submission }: SplitPointsActionsProps) => { +export const SplitPointsActions = ({ vault, submission, linkedPRs }: SplitPointsActionsProps) => { const { t } = useTranslation(); const { address } = useAccount(); const { chain } = useNetwork(); @@ -65,6 +66,7 @@ export const SplitPointsActions = ({ vault, submission }: SplitPointsActionsProp const { data: claimedByProfile } = useProfileByAddress(claimedByInfo?.claimedBy); const isClaimedByCurrentUser = claimedByInfo?.claimedBy.toLowerCase() === address?.toLowerCase(); + const isOpenToSubmissions = linkedPRs.length === 0 ? true : linkedPRs?.every((pr) => pr.bonusSubmissionStatus === "INCOMPLETE"); const canExecuteAction = () => { if (isClaimedByCurrentUser) return { can: true }; @@ -72,6 +74,10 @@ export const SplitPointsActions = ({ vault, submission }: SplitPointsActionsProp if (!isConnected) return { can: false, reason: t("youNeedToConnectYourWallet") }; if (!isInLeadearboardBoundaries || !isProfileCreated) return { can: false, reason: t("youAreNotInTopLeaderboardPercentage") }; + if (!isOpenToSubmissions) { + const isCompleted = linkedPRs.some((pr) => pr.bonusSubmissionStatus === "COMPLETE"); + return { can: false, reason: isCompleted ? t("issueAlreadyHaveValidSubmission") : t("oneSubmissionIsBeingReviewed") }; + } return { can: true }; }; 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 814c6b7ec..f39afc7c2 100644 --- a/packages/web/src/pages/Honeypots/VaultDetailsPage/Sections/VaultSubmissionsSection/VaultSubmissionsSection.tsx +++ b/packages/web/src/pages/Honeypots/VaultDetailsPage/Sections/VaultSubmissionsSection/VaultSubmissionsSection.tsx @@ -5,7 +5,7 @@ import VerifiedIcon from "@mui/icons-material/VerifiedOutlined"; import { Alert, Button, HatSpinner } from "components"; import useConfirm from "hooks/useConfirm"; import { useTranslation } from "react-i18next"; -import { useSavedSubmissions, useVaultRepoName } from "../../hooks"; +import { useGHIssues, useGHPRs, useVaultRepoName } from "../../hooks"; import PublicSubmissionCard from "./PublicSubmissionCard/PublicSubmissionCard"; import { StyledSubmissionsSection } from "./styles"; @@ -19,9 +19,12 @@ export const VaultSubmissionsSection = ({ vault }: VaultSubmissionsSectionProps) const { t } = useTranslation(); const confirm = useConfirm(); - const { data: savedSubmissions, isLoading } = useSavedSubmissions(vault); + const { data: ghIssues, isLoading: isLoadingIssues } = useGHIssues(vault); + const { data: ghPRs, isLoading: isLoadingPRs } = useGHPRs(vault); const { data: repoName } = useVaultRepoName(vault); + console.log(ghPRs); + const isPrivateAudit = vault?.description?.["project-metadata"].isPrivateAudit; const bonusPointsEnabled = vault.description?.["project-metadata"]?.bonusPointsEnabled; @@ -44,7 +47,7 @@ export const VaultSubmissionsSection = ({ vault }: VaultSubmissionsSectionProps) const getBonusPointsSection = () => { if (isPrivateAudit) return null; - if (savedSubmissions?.length === 0) return null; + if (ghIssues?.length === 0) return null; return (
@@ -82,18 +85,23 @@ export const VaultSubmissionsSection = ({ vault }: VaultSubmissionsSectionProps) )} - {!isPrivateAudit && savedSubmissions?.length === 0 && ( + {!isPrivateAudit && ghIssues?.length === 0 && ( {t("thereIsNoPublicSubmission")} )} - {!isPrivateAudit && isLoading && } + {!isPrivateAudit && isLoadingIssues && isLoadingPRs && } - {!isPrivateAudit && savedSubmissions && savedSubmissions?.length > 0 && ( + {!isPrivateAudit && ghIssues && ghIssues?.length > 0 && (
- {savedSubmissions.map((submission) => ( - + {ghIssues.map((submission) => ( + pr.linkedIssueNumber === submission.number) ?? []} + /> ))}
)} diff --git a/packages/web/src/pages/Honeypots/VaultDetailsPage/VaultDetailsPage.tsx b/packages/web/src/pages/Honeypots/VaultDetailsPage/VaultDetailsPage.tsx index 263348c0b..28ce54783 100644 --- a/packages/web/src/pages/Honeypots/VaultDetailsPage/VaultDetailsPage.tsx +++ b/packages/web/src/pages/Honeypots/VaultDetailsPage/VaultDetailsPage.tsx @@ -12,7 +12,7 @@ import { HoneypotsRoutePaths } from "../router"; import { VaultDepositsSection, VaultRewardsSection, VaultScopeSection, VaultSubmissionsSection } from "./Sections"; import { VaultLeaderboardSection } from "./Sections/VaultLeaderboardSection/VaultLeaderboardSection"; import { EulerCTFTAndC } from "./extra/EulerCTFTAndC"; -import { useCollectMessageSignature, useSavedSubmissions, useUserHasCollectedSignature } from "./hooks"; +import { useCollectMessageSignature, useGHIssues, useUserHasCollectedSignature } from "./hooks"; import { StyledSectionTab, StyledVaultDetailsPage } from "./styles"; const DETAILS_SECTIONS = [ @@ -80,7 +80,7 @@ export const VaultDetailsPage = ({ vaultToUse, noActions = false, noDeployed = f error: errorCollectingSig, } = useCollectMessageSignature(); - const { data: savedSubmissions } = useSavedSubmissions(vault); + const { data: savedSubmissions } = useGHIssues(vault); const { finished: finishedAuditPayouts } = useAuditCompetitionsVaults(); const oldAudits = useOldAuditCompetitions(); const allFinishedAuditCompetitions = [...finishedAuditPayouts, ...(oldAudits ?? [])]; diff --git a/packages/web/src/pages/Honeypots/VaultDetailsPage/hooks.ts b/packages/web/src/pages/Honeypots/VaultDetailsPage/hooks.ts index 386176bae..c578c4fe3 100644 --- a/packages/web/src/pages/Honeypots/VaultDetailsPage/hooks.ts +++ b/packages/web/src/pages/Honeypots/VaultDetailsPage/hooks.ts @@ -1,7 +1,7 @@ -import { GithubIssue, IVault } from "@hats.finance/shared"; +import { GithubIssue, GithubPR, 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 { getGithubIssuesFromVault, getGithubPRsFromVault } from "pages/CommitteeTools/SubmissionsTool/submissionsService.api"; import { useAccount, useMutation } from "wagmi"; import * as messageSignaturesService from "./messageSignaturesService"; import * as savedSubmissionsService from "./savedSubmissionsService"; @@ -9,7 +9,7 @@ import * as savedSubmissionsService from "./savedSubmissionsService"; /** * Returns all saved submissions for a vault */ -export const useSavedSubmissions = (vault: IVault | undefined): UseQueryResult => { +export const useGHIssues = (vault: IVault | undefined): UseQueryResult => { return useQuery({ queryKey: ["github-issues", vault?.id], queryFn: () => getGithubIssuesFromVault(vault!), @@ -18,6 +18,15 @@ export const useSavedSubmissions = (vault: IVault | undefined): UseQueryResult => { + return useQuery({ + queryKey: ["github-prs", vault?.id], + queryFn: () => getGithubPRsFromVault(vault!), + refetchOnWindowFocus: false, + enabled: !!vault, + }); +}; + /** * Returns the repo name created for a vault */ 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 fb97f35da..1e5b30451 100644 --- a/packages/web/src/pages/Submissions/SubmissionFormPage/FormSteps/SubmissionDescriptions/utils.ts +++ b/packages/web/src/pages/Submissions/SubmissionFormPage/FormSteps/SubmissionDescriptions/utils.ts @@ -95,7 +95,7 @@ export const getBountySubmissionTexts = ( 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 +**HATS Profile:** ${hackerProfile ? `[HATS Profile](https://app.hats.finance/profile/${hackerProfile?.username})` : "---"}\n **Communication channel:** ${submissionData.contact?.communicationChannel} (${submissionData.contact?.communicationChannelType}) ${descriptions @@ -125,7 +125,7 @@ export const getGithubIssueDescription = ( 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}` : "---"} + **HATS Profile:** ${hackerProfile ? `[HATS Profile](https://app.hats.finance/profile/${hackerProfile?.username})` : "---"}\n **Beneficiary:** ${submissionData.contact?.beneficiary} **Submission hash (on-chain):** ${submissionData.submissionResult?.transactionHash} **Severity:** ${description.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 2ac9c2b0d..a5393f5a5 100644 --- a/packages/web/src/pages/VaultEditor/VaultEditorFormPage/SetupSteps/VaultDetailsForm/VaultDetailsForm.tsx +++ b/packages/web/src/pages/VaultEditor/VaultEditorFormPage/SetupSteps/VaultDetailsForm/VaultDetailsForm.tsx @@ -204,6 +204,17 @@ export function VaultDetailsForm() { helper={watch("project-metadata.oneLiner") ? `${watch("project-metadata.oneLiner")?.length ?? 0} characters` : ""} /> +
+ +
+ {requireMessageSignature && ( <>

{t("VaultEditor.vault-details.messageToSign-explanation")}