From 459f51c5743ab071c7f7fcbd01f980d923e55ce0 Mon Sep 17 00:00:00 2001 From: Carlos Fontes Date: Tue, 3 Dec 2024 20:18:08 +0000 Subject: [PATCH] chore: bonus points payments --- packages/shared/src/types/payout.ts | 6 ++- .../PayoutFormPage/PayoutFormPage.tsx | 7 +++ .../components/SplitPayoutBeneficiaryForm.tsx | 44 ++++++++++++--- .../utils/autocalculateMultiPayout.ts | 52 ++++++++++++++---- .../SubmissionsListPage/SubmissionCard.tsx | 54 +++++++++++++++---- .../SubmissionsListPage.tsx | 43 ++++++++++++--- .../SubmissionsTool/submissionsService.api.ts | 33 +++++++++++- 7 files changed, 200 insertions(+), 39 deletions(-) diff --git a/packages/shared/src/types/payout.ts b/packages/shared/src/types/payout.ts index a0909c2b6..7bd1855bf 100644 --- a/packages/shared/src/types/payout.ts +++ b/packages/shared/src/types/payout.ts @@ -52,6 +52,7 @@ export type GithubIssue = { createdAt: string; body: string; txHash?: string; + severity?: string; bonusPointsLabels: { needsFix: boolean; needsTest: boolean; @@ -69,6 +70,7 @@ export type GithubPR = { txHash?: string; bonusSubmissionStatus: "COMPLETE" | "INCOMPLETE" | "PENDING"; linkedIssueNumber?: number; + linkedIssue?: GithubIssue; }; export type IPayoutData = ISinglePayoutData | ISplitPayoutData; @@ -93,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 @@ -119,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/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..e58fc60cf 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,15 @@ export const SplitPayoutBeneficiaryForm = ({ ]; }; + console.log( + getGhIssueFromSubmission( + isPayoutCreated ? beneficiaries[index]?.decryptedSubmission ?? beneficiarySubmission! : beneficiarySubmission!, + vaultGithubIssues + ) + ); + console.log(isPayoutCreated ? beneficiaries[index]?.decryptedSubmission ?? beneficiarySubmission! : beneficiarySubmission!); + console.log(vaultGithubIssues); + return (
{index + 1}.
@@ -166,10 +187,21 @@ export const SplitPayoutBeneficiaryForm = ({ submission={ isPayoutCreated ? beneficiaries[index]?.decryptedSubmission ?? beneficiarySubmission! : beneficiarySubmission! } - ghIssue={getGhIssueFromSubmission( - isPayoutCreated ? beneficiaries[index]?.decryptedSubmission ?? beneficiarySubmission! : beneficiarySubmission!, - vaultGithubIssues - )} + ghIssue={ + getGhIssueFromSubmission( + isPayoutCreated + ? beneficiaries[index]?.decryptedSubmission ?? beneficiarySubmission! + : beneficiarySubmission!, + vaultGithubIssues + ) || + getGhPRFromSubmission( + isPayoutCreated + ? beneficiaries[index]?.decryptedSubmission ?? beneficiarySubmission! + : beneficiarySubmission!, + 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..8e9ef0948 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[]; @@ -94,20 +99,45 @@ export const autocalculateMultiPayoutPointingSystem = ( if (!constraints || !constraints.length) return undefined; if (!beneficiaries || beneficiaries.length === 0) return undefined; + console.log({ beneficiaries, constraints, totalAmountToPay, maxCapPerPoint }); + const beneficiariesCalculated = [] as IBeneficiaryWithCalcs[]; 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..1ee68e379 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,8 @@ export const SubmissionCard = ({ const commChannel = submissionData?.communicationChannel; const severityColors = getSeveritiesColorsArray(vault); + const isComplementary = !!(ghIssue as GithubPR)?.linkedIssueNumber; + const createdAt = new Date(+submission?.createdAt * 1000); const severityIndex = @@ -67,25 +69,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..59f54da56 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) + } /> ); })} diff --git a/packages/web/src/pages/CommitteeTools/SubmissionsTool/submissionsService.api.ts b/packages/web/src/pages/CommitteeTools/SubmissionsTool/submissionsService.api.ts index 9f2833c9b..04f42cd61 100644 --- a/packages/web/src/pages/CommitteeTools/SubmissionsTool/submissionsService.api.ts +++ b/packages/web/src/pages/CommitteeTools/SubmissionsTool/submissionsService.api.ts @@ -140,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 ?? "--", @@ -237,7 +239,10 @@ export async function getGithubPRsFromVault(vault: IVault): Promise createdAt: pr.created_at, body: pr.body, txHash: extractTxHashFromBody(pr), - bonusSubmissionStatus: pr.labels.some((label: any) => (label.name as string).toLowerCase() === "complete") + 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" @@ -295,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], + }, + }; +}