Skip to content

Commit

Permalink
Merge pull request #785 from hats-finance/feat/bonus-points-payments
Browse files Browse the repository at this point in the history
Feat/bonus points payments
  • Loading branch information
shayzluf authored Dec 12, 2024
2 parents 5bc9ce1 + c93f3bd commit f098432
Show file tree
Hide file tree
Showing 13 changed files with 283 additions and 56 deletions.
19 changes: 17 additions & 2 deletions packages/shared/src/types/payout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -80,7 +95,7 @@ export interface ISinglePayoutData extends IPayoutDataBase {
nftUrl: string;
submissionData?: { id: string; subId: string; idx: number };
decryptedSubmission?: Omit<ISubmittedSubmission, "linkedVault">; // Omit: workaround to avoid circular dependency;
ghIssue?: GithubIssue;
ghIssue?: GithubIssue | GithubPR;
}

// Only for v2 vaults
Expand All @@ -106,7 +121,7 @@ export interface ISplitPayoutBeneficiary {
nftUrl: string;
submissionData?: { id: string; subId: string; idx: number };
decryptedSubmission?: Omit<ISubmittedSubmission, "linkedVault">; // Omit: workaround to avoid circular dependency;
ghIssue?: GithubIssue;
ghIssue?: GithubIssue | GithubPR;
}

export interface IPayoutSignature {
Expand Down
2 changes: 2 additions & 0 deletions packages/web/src/languages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -749,6 +749,8 @@
"bonusPointsReminder": "Remember, you have 12 hours to submit after claiming. Happy fixing and testing!",
"bonusPointsEnabled": "Enable bonus points?",
"claimFixAndTest": "Claim fix and test",
"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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 &&
Expand Down
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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";
Expand Down Expand Up @@ -99,6 +104,7 @@ export const SplitPayoutBeneficiaryForm = ({
});

const [vaultGithubIssues, setVaultGithubIssues] = useState<GithubIssue[] | undefined>(undefined);
const [vaultGithubPRs, setVaultGithubPRs] = useState<GithubPR[] | undefined>(undefined);
const [isLoadingGH, setIsLoadingGH] = useState<boolean>(false);

// Get information from github
Expand All @@ -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 = () => {
Expand Down Expand Up @@ -153,6 +165,10 @@ export const SplitPayoutBeneficiaryForm = ({
];
};

const selectedSubmission = isPayoutCreated
? beneficiaries[index]?.decryptedSubmission ?? beneficiarySubmission!
: beneficiarySubmission!;

return (
<div>
<div className="mb-1">{index + 1}.</div>
Expand All @@ -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)
}
/>
</div>
) : (
Expand Down
Original file line number Diff line number Diff line change
@@ -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[];
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -18,7 +18,7 @@ type SubmissionCardProps = {
inPayout?: boolean;
isChecked?: boolean;
onCheckChange?: (submission: ISubmittedSubmission) => void;
ghIssue?: GithubIssue;
ghIssue?: GithubIssue | GithubPR;
};

export const SubmissionCard = ({
Expand All @@ -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 =
Expand Down Expand Up @@ -67,25 +73,55 @@ export const SubmissionCard = ({
<div className="content">
{submissionData?.severity && (
<span className="severity">
{ghIssue && (
{ghIssue && !isComplementary && (
<span>
{t("ghIssue")} #{ghIssue.number} -
</span>
)}
<span>{t("submittedAs")}:</span>
{!isComplementary && <span>{t("submittedAs")}:</span>}
<Pill
textColor={severityColors[severityIndex ?? 0]}
isSeverity={!ghIssue}
text={parseSeverityName(submissionData?.severity) ?? t("noSeverity")}
/>
{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 (
<>
<span>{t("status")}:</span>
<Pill
textColor={(ghIssue as GithubPR).bonusSubmissionStatus === "COMPLETE" ? "#3ee136" : "#bab441"}
isSeverity
text={getText()}
/>
</>
);
})()
) : (
<>
<span>{t("labeledAs")}:</span>
<Pill
textColor={severityColors[severityIndex ?? 0]}
isSeverity
text={parseSeverityName(ghIssue.validLabels[0])}
/>
{ghIssue && (ghIssue as GithubIssue)?.validLabels?.length > 0 && (
<>
<span>{t("labeledAs")}:</span>
<Pill
textColor={severityColors[severityIndex ?? 0]}
isSeverity
text={parseSeverityName((ghIssue as GithubIssue).validLabels[0])}
/>
</>
)}
</>
)}
</span>
Expand Down
Loading

0 comments on commit f098432

Please sign in to comment.