From e55889a9910cb8260e23377e9cfccdc5a2703571 Mon Sep 17 00:00:00 2001 From: Fran Rimoldi Date: Wed, 9 Aug 2023 17:47:57 -0300 Subject: [PATCH 01/80] feat: display nsloc diff --- src/components/Text/Text.module.scss | 4 + src/components/Text/Text.tsx | 2 +- src/hooks/api/admin/useAdminContestScope.ts | 14 ++- src/hooks/api/scope/useDeleteScope.ts | 2 +- src/hooks/api/scope/useRepositoryContracts.ts | 103 +++++++++++++----- src/hooks/api/scope/useScope.ts | 56 ++++++---- src/hooks/api/scope/useUpdateScope.ts | 14 ++- src/pages/AuditScope/AuditScope.module.scss | 5 + src/pages/AuditScope/AuditScope.tsx | 84 +++++++++----- src/pages/AuditScope/AuditScopeReadOnly.tsx | 4 +- .../RepositoryContractsSelector.tsx | 103 ++++++++++-------- src/pages/AuditScope/ScopeList.tsx | 15 ++- .../AdminContestsList/ContestScopeModal.tsx | 6 +- src/pages/admin/AdminScope/AdminScope.tsx | 29 ++--- 14 files changed, 296 insertions(+), 145 deletions(-) diff --git a/src/components/Text/Text.module.scss b/src/components/Text/Text.module.scss index 1f36b17e..9c818fb5 100644 --- a/src/components/Text/Text.module.scss +++ b/src/components/Text/Text.module.scss @@ -7,6 +7,10 @@ color: red; } + &.success { + color: #58c322; + } + &.strong { font-weight: 700; } diff --git a/src/components/Text/Text.tsx b/src/components/Text/Text.tsx index 73537ee6..e8dc9475 100644 --- a/src/components/Text/Text.tsx +++ b/src/components/Text/Text.tsx @@ -5,7 +5,7 @@ import styles from "./Text.module.scss" type TextSize = "tiny" | "small" | "normal" | "large" | "extra-large" -type TextVariant = "normal" | "primary" | "secondary" | "alternate" | "mono" | "warning" +type TextVariant = "normal" | "primary" | "secondary" | "alternate" | "mono" | "warning" | "success" type TextAlignment = "center" | "left" | "right" | "justify" diff --git a/src/hooks/api/admin/useAdminContestScope.ts b/src/hooks/api/admin/useAdminContestScope.ts index 5762b50c..730abb2f 100644 --- a/src/hooks/api/admin/useAdminContestScope.ts +++ b/src/hooks/api/admin/useAdminContestScope.ts @@ -8,7 +8,11 @@ type AdminContestScopeResponse = { repo_name: string branch_name: string commit_hash: string - files: string[] + files: { + file_path: string + nsloc?: number + selected: boolean + }[] solidity_metrics_report: string nsloc: number comment_to_source_ratio: number @@ -17,14 +21,18 @@ type AdminContestScopeResponse = { export const adminContestScopeQuery = (contestID: number) => ["admin-contest-scope", contestID] export const useAdminContestScope = (contestID: number) => - useQuery(adminContestScopeQuery(contestID), async () => { + useQuery(adminContestScopeQuery(contestID), async () => { const { data } = await contestsAPI.get(getAdminContestScopeUrl(contestID)) return data.scope.map((d) => ({ repoName: d.repo_name, branchName: d.branch_name, commitHash: d.commit_hash, - files: d.files, + files: d.files.map((f) => ({ + filePath: f.file_path, + nSLOC: f.nsloc, + selected: f.selected, + })), solidityMetricsReport: d.solidity_metrics_report, nSLOC: d.nsloc, commentToSourceRatio: d.comment_to_source_ratio, diff --git a/src/hooks/api/scope/useDeleteScope.ts b/src/hooks/api/scope/useDeleteScope.ts index 6ab4e3ad..7049ef2c 100644 --- a/src/hooks/api/scope/useDeleteScope.ts +++ b/src/hooks/api/scope/useDeleteScope.ts @@ -36,7 +36,7 @@ export const useDeleteScope = () => { const previousScope = queryClient.getQueryData(scopeQueryKey(params.protocolDashboardID)) - queryClient.setQueryData(scopeQueryKey(params.protocolDashboardID), (previous) => + queryClient.setQueryData(scopeQueryKey(params.protocolDashboardID), (previous) => previous?.filter((s) => s.repoName !== params.repoName) ) diff --git a/src/hooks/api/scope/useRepositoryContracts.ts b/src/hooks/api/scope/useRepositoryContracts.ts index c54007c8..cc6cfbcb 100644 --- a/src/hooks/api/scope/useRepositoryContracts.ts +++ b/src/hooks/api/scope/useRepositoryContracts.ts @@ -4,53 +4,104 @@ import { getRepositoryContracts as getRepositoryContractsUrl } from "../urls" type GetRepositoryContractsResponse = string[] -export type TreeValue = Tree | string -export interface Tree extends Map {} - -export const getAllTreePaths = (parentPath: string, tree: TreeValue) => { - if (typeof tree === "string") { - return [`${parentPath}`] - } +type File = { + filepath: string + nsloc?: number +} - let paths: string[] = [] +interface BaseEntry { + name: string +} - tree.forEach((value, key) => { - paths = [...paths, ...getAllTreePaths(`${parentPath}/${key}`, value)] - }) +interface FileEntry extends BaseEntry { + type: "file" + filepath: string + nsloc?: number +} - return paths +interface DirectoryEntry extends BaseEntry { + type: "directory" + entries: Array } -type Files = { - tree: Tree - rawPaths: string[] +export type Entry = FileEntry | DirectoryEntry + +type AnyEntry = RootDirectory | DirectoryEntry + +export interface RootDirectory { + type: "root" + entries: Array } -export const convertToTree = (paths: string[]) => { - const tree: Tree = new Map() +export const convertToTree2 = (files: File[]) => { + const root: RootDirectory = { + type: "root", + entries: [], + } + + files.forEach(({ filepath, nsloc }) => { + const parts = filepath.split("/") + + let current: AnyEntry = root - paths.forEach((d) => { - const parts = d.split("/") - let current: Tree = tree parts.forEach((p) => { if (p.endsWith(".sol") || p.endsWith(".vy")) { - current.set(p, p) + current.entries.push({ + type: "file", + name: p, + filepath, + nsloc, + }) } else { - if (!current.get(p)) current.set(p, new Map()) - current = current.get(p) as Tree + let directory: DirectoryEntry | undefined = current.entries.find( + (e) => e.type === "directory" && e.name === p + ) as DirectoryEntry + + if (!directory) { + directory = { + type: "directory", + name: p, + entries: [], + } + current.entries.push(directory) + } + + current = directory } }) }) - return tree + return root +} + +export type TreeValue = Tree | string +export interface Tree extends Map {} + +export const getAllTreePaths = (parentPath: string, tree: Entry) => { + if (tree.type === "file") { + return [`${parentPath}`] + } + + let paths: string[] = [] + + tree.entries.forEach((value, key) => { + paths = [...paths, ...getAllTreePaths(`${parentPath}/${value.name}`, value)] + }) + + return paths +} + +type RepositoryContracts = { + tree: RootDirectory + rawPaths: string[] } export const repositoryContractsQuery = (repo: string, commit: string) => ["repository-contracts", repo, commit] export const useRepositoryContracts = (repo: string, commit: string) => - useQuery(repositoryContractsQuery(repo, commit), async () => { + useQuery(repositoryContractsQuery(repo, commit), async () => { const { data } = await contestsAPI.get(getRepositoryContractsUrl(repo, commit)) - const tree = convertToTree(data) + const tree = convertToTree2(data.map((f) => ({ filepath: f }))) return { tree, diff --git a/src/hooks/api/scope/useScope.ts b/src/hooks/api/scope/useScope.ts index 68ffd22a..2e912d10 100644 --- a/src/hooks/api/scope/useScope.ts +++ b/src/hooks/api/scope/useScope.ts @@ -6,38 +6,56 @@ export type Scope = { repoName: string branchName: string commitHash: string - files: string[] + files: { + filePath: string + nSLOC?: number + selected: boolean + }[] solidityMetricsReport?: string - nSLOC?: number commentToSourceRatio?: number -}[] + initialScope?: Scope +} -export type GetScopeResponse = { - scope: { - repo_name: string - branch_name: string - commit_hash: string - files: string[] +type ScopeResponse = { + repo_name: string + branch_name: string + commit_hash: string + files: { + file_path: string nsloc?: number - comment_to_source_ratio?: number + selected: boolean }[] + comment_to_source_ratio?: number + initial_scope?: Omit +} + +export type GetScopeResponse = { + scope: ScopeResponse[] +} + +function parseScope(d: ScopeResponse): Scope { + return { + repoName: d.repo_name, + branchName: d.branch_name, + commitHash: d.commit_hash, + files: d.files.map((f) => ({ + filePath: f.file_path, + nSLOC: f.nsloc, + selected: f.selected, + })), + commentToSourceRatio: d.comment_to_source_ratio, + initialScope: d.initial_scope ? parseScope(d.initial_scope) : undefined, + } } export const scopeQueryKey = (dashboardID?: string) => ["scope", dashboardID] export const useScope = (dashboardID?: string) => - useQuery( + useQuery( scopeQueryKey(dashboardID), async () => { const { data } = await contestsAPI.get(getScopeUrl(dashboardID ?? "")) - return data.scope.map((d) => ({ - repoName: d.repo_name, - branchName: d.branch_name, - commitHash: d.commit_hash, - files: d.files, - nSLOC: d.nsloc, - commentToSourceRatio: d.comment_to_source_ratio, - })) + return data.scope.map(parseScope) }, { enabled: !!dashboardID, diff --git a/src/hooks/api/scope/useUpdateScope.ts b/src/hooks/api/scope/useUpdateScope.ts index 07b862f3..ab7f2176 100644 --- a/src/hooks/api/scope/useUpdateScope.ts +++ b/src/hooks/api/scope/useUpdateScope.ts @@ -14,7 +14,7 @@ type UpdateScopeParams = { } type UpdateScopeContext = { - previousScope?: Scope + previousScope?: Scope[] } export const useUpdateScope = () => { @@ -40,9 +40,9 @@ export const useUpdateScope = () => { onMutate: async (params) => { await queryClient.invalidateQueries(scopeQueryKey(params.protocolDashboardID)) - const previousScope = queryClient.getQueryData(scopeQueryKey(params.protocolDashboardID)) + const previousScope = queryClient.getQueryData(scopeQueryKey(params.protocolDashboardID)) - queryClient.setQueryData(scopeQueryKey(params.protocolDashboardID), (previous) => { + queryClient.setQueryData(scopeQueryKey(params.protocolDashboardID), (previous) => { if (!previous) return const scopeIndex = previous.findIndex((s) => s.repoName === params.repoName) @@ -54,7 +54,13 @@ export const useUpdateScope = () => { repoName: previous[scopeIndex].repoName, branchName: params.branchName ?? previous[scopeIndex].branchName, commitHash: params.commitHash ?? previous[scopeIndex].commitHash, - files: params.files ?? previous[scopeIndex].files, + files: params.files + ? previous[scopeIndex].files.map((f) => ({ + ...f, + selected: params.files!.includes(f.filePath), + })) + : previous[scopeIndex].files, + initialScope: previous[scopeIndex].initialScope, }, ...previous.slice(scopeIndex + 1), ] diff --git a/src/pages/AuditScope/AuditScope.module.scss b/src/pages/AuditScope/AuditScope.module.scss index 1359cf25..c623df43 100644 --- a/src/pages/AuditScope/AuditScope.module.scss +++ b/src/pages/AuditScope/AuditScope.module.scss @@ -77,6 +77,11 @@ opacity: 1; } } + + .addedNSLOC { + color: #58c322; + font-weight: bold; + } } } } diff --git a/src/pages/AuditScope/AuditScope.tsx b/src/pages/AuditScope/AuditScope.tsx index 02df44a4..8765a13b 100644 --- a/src/pages/AuditScope/AuditScope.tsx +++ b/src/pages/AuditScope/AuditScope.tsx @@ -21,6 +21,9 @@ import { AuditScopeReadOnly } from "./AuditScopeReadOnly" import Modal, { Props as ModalProps } from "../../components/Modal/Modal" import { useSubmitScope } from "../../hooks/api/contests/useSubmitScope" import { ErrorModal } from "../ContestDetails/ErrorModal" +import { convertToTree2 } from "../../hooks/api/scope/useRepositoryContracts" + +import styles from "./AuditScope.module.scss" type Props = ModalProps & { dashboardID: string @@ -106,8 +109,10 @@ export const AuditScope = () => { const handlePathSelected = useCallback( (repo: string, paths: string[]) => { - console.log(paths) - let selectedPaths = scope?.find((s) => s.repoName === repo)?.files + let selectedPaths = scope + ?.find((s) => s.repoName === repo) + ?.files.filter((f) => f.selected) + .map((f) => f.filePath) if (!selectedPaths) return @@ -179,14 +184,14 @@ export const AuditScope = () => { ) const handleSelectAll = useCallback( - (repoName: string, files: string[]) => { - const repo = scope?.find((s) => s.repoName === repoName) - if (!repo) return + (repoName: string) => { + const s = scope?.find((s) => s.repoName === repoName) + if (!s) return updateScope({ protocolDashboardID: dashboardID ?? "", repoName, - files, + files: s.files.map((f) => f.filePath), }) }, [updateScope, dashboardID, scope] @@ -281,25 +286,54 @@ export const AuditScope = () => { ) : null} - {scope?.map((s) => ( - - - - <FaGithub /> -   - {s.repoName} - - handlePathSelected(s.repoName, paths)} - selectedPaths={s.files} - onSelectAll={(files) => handleSelectAll(s.repoName, files)} - onClearSelection={() => handleClearSelection(s.repoName)} - /> - - - ))} + {scope?.map((s) => { + const selectedNSLOC = s.files.reduce((t, f) => (t += f.selected ? f.nSLOC ?? 0 : 0), 0) + const initialNSLOC = s.initialScope?.files.reduce((t, f) => (t += f.selected ? f.nSLOC ?? 0 : 0), 0) + const differenceNSLOC = initialNSLOC && selectedNSLOC - initialNSLOC + + return ( + + + + <FaGithub /> +   + {s.repoName} + + + + Selected files: {s.files.filter((f) => f.selected).length} + + + nSLOC: {s.files.reduce((t, f) => (t += f.selected ? f.nSLOC ?? 0 : 0), 0)} + + + {differenceNSLOC ? ( + + + Difference with initial scoping: + + + {differenceNSLOC} nSLOC + + + ) : null} + ({ + filepath: f.filePath, + nsloc: f.nSLOC ?? 0, + })) ?? [] + )} + onPathSelected={(paths) => handlePathSelected(s.repoName, paths)} + selectedPaths={s.files.filter((f) => f.selected).map((f) => f.filePath)} + onSelectAll={() => handleSelectAll(s.repoName)} + onClearSelection={() => handleClearSelection(s.repoName)} + initialScope={s.initialScope} + /> + + + ) + })} @@ -182,6 +187,18 @@ export const RepositoryContractsSelector: React.FC = ({ Clear selection + + + Files + + + + nSLOC + + (Diff.) + + +
    {treeElements}
) diff --git a/src/pages/AuditScope/ScopeList.tsx b/src/pages/AuditScope/ScopeList.tsx index 74ea4774..249a2f3f 100644 --- a/src/pages/AuditScope/ScopeList.tsx +++ b/src/pages/AuditScope/ScopeList.tsx @@ -3,25 +3,30 @@ import { Box } from "../../components/Box" import { Column } from "../../components/Layout" import { Text } from "../../components/Text" import { Title } from "../../components/Title" -import { convertToTree } from "../../hooks/api/scope/useRepositoryContracts" +import { convertToTree2 } from "../../hooks/api/scope/useRepositoryContracts" import { Scope } from "../../hooks/api/scope/useScope" import { TreeEntry } from "./RepositoryContractsSelector" import styles from "./AuditScope.module.scss" type Props = { - scope: Scope + scope: Scope[] } export const ScopeList: React.FC = ({ scope }) => { return ( {scope?.map((s) => { - const tree = convertToTree(s.files) + const tree = convertToTree2( + s.files.map((f) => ({ + filepath: f.filePath, + nsloc: f.nSLOC ?? 0, + })) ?? [] + ) const treeElements: React.ReactNode[] = [] - tree.forEach((value, key) => { - treeElements.push() + tree.entries.forEach((value, key) => { + treeElements.push() }) return ( diff --git a/src/pages/admin/AdminContestsList/ContestScopeModal.tsx b/src/pages/admin/AdminContestsList/ContestScopeModal.tsx index 38f03b6c..5908a2f2 100644 --- a/src/pages/admin/AdminContestsList/ContestScopeModal.tsx +++ b/src/pages/admin/AdminContestsList/ContestScopeModal.tsx @@ -22,8 +22,8 @@ export const ContestScopeModal: React.FC = ({ onClose, contestID }) => { const { data: contest, isLoading: contestIsLoading } = useContest(contestID) const { data: scope, isLoading: scopeIsLoading } = useAdminContestScope(contestID) - const submittedNSLOC = scope?.reduce((t, s) => t + (s.nSLOC ?? 0), 0) - const expectedNSLOCExceeded = contest && scope && (contest.linesOfCode ?? 0) < (submittedNSLOC ?? 0) + const submittedNSLOC = scope?.reduce((t, s) => t + (s.files.reduce((t, f) => t + (f.nSLOC ?? 0), 0) ?? 0), 0) ?? 0 + const expectedNSLOCExceeded = contest && scope && (parseInt(contest.linesOfCode ?? "") ?? 0) < (submittedNSLOC ?? 0) return ( @@ -68,7 +68,7 @@ export const ContestScopeModal: React.FC = ({ onClose, contestID }) => { nSLOC: - {s.nSLOC} + {s.files.reduce((t, f) => t + (f.nSLOC ?? 0), 0)} diff --git a/src/pages/admin/AdminScope/AdminScope.tsx b/src/pages/admin/AdminScope/AdminScope.tsx index 64762eec..a305d1b6 100644 --- a/src/pages/admin/AdminScope/AdminScope.tsx +++ b/src/pages/admin/AdminScope/AdminScope.tsx @@ -14,6 +14,7 @@ import { shortenCommitHash } from "../../../utils/repository" import { BranchSelectionModal } from "../../AuditScope/BranchSelectionModal" import { CommitSelectionModal } from "../../AuditScope/CommitSelectionModal" import { RepositoryContractsSelector } from "../../AuditScope/RepositoryContractsSelector" +import { useRepositoryContracts } from "../../../hooks/api/scope/useRepositoryContracts" export const AdminScope = () => { const [repoLink, setRepoLink] = useState("") @@ -29,6 +30,8 @@ export const AdminScope = () => { const { data: repo, isLoading: repoIsLoading } = useRepository(debouncedRepoName) const { submitScope, isLoading, data: report } = useAdminSubmitScope() + const { data: repoContracts } = useRepositoryContracts(debouncedRepoName, commitHash ?? "") + useEffect(() => { const pattern = /^https?:\/\/github\.com\/([A-Za-z0-9-]+\/[A-Za-z0-9-]+)(?:\.git)?(?:\/tree\/([A-Za-z0-9-]+))?$/ const match = repoLink.match(pattern) @@ -75,12 +78,9 @@ export const AdminScope = () => { [setCommitHash] ) - const handleSelectAll = useCallback( - (selectedPaths: string[]) => { - setFiles(selectedPaths) - }, - [setFiles] - ) + const handleSelectAll = useCallback(() => { + setFiles(repoContracts?.rawPaths ?? []) + }, [repoContracts]) const handleClearSelection = useCallback(() => { setFiles([]) @@ -177,14 +177,15 @@ export const AdminScope = () => {   {repo.name} - + {repoContracts ? ( + + ) : null}
)} From 9d0f460163873893abb139f40b849bfaae4c6df1 Mon Sep 17 00:00:00 2001 From: Fran Rimoldi Date: Thu, 10 Aug 2023 13:40:42 -0300 Subject: [PATCH 02/80] feat: scope diff modal --- src/pages/AuditScope/ScopeDiffModal.tsx | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 src/pages/AuditScope/ScopeDiffModal.tsx diff --git a/src/pages/AuditScope/ScopeDiffModal.tsx b/src/pages/AuditScope/ScopeDiffModal.tsx new file mode 100644 index 00000000..5fcb8f72 --- /dev/null +++ b/src/pages/AuditScope/ScopeDiffModal.tsx @@ -0,0 +1,7 @@ +import Modal, { Props as ModalProps } from "../../components/Modal/Modal" + +type Props = ModalProps & {} + +export const ScopeDiffModal: React.FC = ({ onClose }) => { + return +} From be9b6dbb24b136afa986000802a676ee732663c8 Mon Sep 17 00:00:00 2001 From: Fran Rimoldi Date: Thu, 10 Aug 2023 15:11:17 -0300 Subject: [PATCH 03/80] feat: update protocols slug --- src/pages/Protocol/Protocol.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pages/Protocol/Protocol.tsx b/src/pages/Protocol/Protocol.tsx index 94eadbb6..e67dc4f3 100644 --- a/src/pages/Protocol/Protocol.tsx +++ b/src/pages/Protocol/Protocol.tsx @@ -59,7 +59,9 @@ export const ProtocolPage: React.FC = () => { if (!protocols) return if (protocolTag) { - const found = Object.entries(protocols).find(([_, p]) => p.tag === protocolTag) + const found = Object.entries(protocols).find( + ([_, p]) => p.name.toLowerCase().replaceAll(" ", "_") === protocolTag + ) if (found) { const [_, protocol] = found From 1490dfe17cb6971291c9a17b071644e9f13c4384 Mon Sep 17 00:00:00 2001 From: Fran Rimoldi Date: Thu, 10 Aug 2023 15:28:51 -0300 Subject: [PATCH 04/80] fix: protocol name could be undefined --- src/pages/Protocol/Protocol.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/Protocol/Protocol.tsx b/src/pages/Protocol/Protocol.tsx index e67dc4f3..9ebab692 100644 --- a/src/pages/Protocol/Protocol.tsx +++ b/src/pages/Protocol/Protocol.tsx @@ -60,7 +60,7 @@ export const ProtocolPage: React.FC = () => { if (protocolTag) { const found = Object.entries(protocols).find( - ([_, p]) => p.name.toLowerCase().replaceAll(" ", "_") === protocolTag + ([_, p]) => p.name?.toLowerCase().replaceAll(" ", "_") === protocolTag ) if (found) { From c037f9548afbfdf40aeee31898d86bca5d6089a2 Mon Sep 17 00:00:00 2001 From: Fran Rimoldi Date: Thu, 10 Aug 2023 15:32:45 -0300 Subject: [PATCH 05/80] fix --- src/pages/Protocol/Protocol.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/Protocol/Protocol.tsx b/src/pages/Protocol/Protocol.tsx index 9ebab692..7364c6ba 100644 --- a/src/pages/Protocol/Protocol.tsx +++ b/src/pages/Protocol/Protocol.tsx @@ -60,7 +60,7 @@ export const ProtocolPage: React.FC = () => { if (protocolTag) { const found = Object.entries(protocols).find( - ([_, p]) => p.name?.toLowerCase().replaceAll(" ", "_") === protocolTag + ([_, p]) => p.name && p.name.toLowerCase().replaceAll(" ", "_") === protocolTag ) if (found) { From 49d18ed1e50e5a09a9333d2cfa73915d37ca41c4 Mon Sep 17 00:00:00 2001 From: Fran Rimoldi Date: Fri, 11 Aug 2023 08:22:39 -0300 Subject: [PATCH 06/80] fix: protocols slugs --- src/pages/Protocol/Protocol.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/Protocol/Protocol.tsx b/src/pages/Protocol/Protocol.tsx index 7364c6ba..8f495cf7 100644 --- a/src/pages/Protocol/Protocol.tsx +++ b/src/pages/Protocol/Protocol.tsx @@ -60,7 +60,7 @@ export const ProtocolPage: React.FC = () => { if (protocolTag) { const found = Object.entries(protocols).find( - ([_, p]) => p.name && p.name.toLowerCase().replaceAll(" ", "_") === protocolTag + ([_, p]) => (p.name && p.name.toLowerCase().replaceAll(" ", "_") === protocolTag) || p.tag === protocolTag ) if (found) { From 2a0795227ccc9912b3261672604089728f6760ba Mon Sep 17 00:00:00 2001 From: Fran Rimoldi Date: Sun, 13 Aug 2023 10:42:30 -0300 Subject: [PATCH 07/80] feat: icons and color --- src/hooks/api/scope/useRepositoryContracts.ts | 2 +- src/pages/AuditScope/AuditScope.module.scss | 13 ++++++ .../RepositoryContractsSelector.tsx | 41 ++++++++++++++++--- 3 files changed, 49 insertions(+), 7 deletions(-) diff --git a/src/hooks/api/scope/useRepositoryContracts.ts b/src/hooks/api/scope/useRepositoryContracts.ts index cc6cfbcb..5511cf24 100644 --- a/src/hooks/api/scope/useRepositoryContracts.ts +++ b/src/hooks/api/scope/useRepositoryContracts.ts @@ -13,7 +13,7 @@ interface BaseEntry { name: string } -interface FileEntry extends BaseEntry { +export interface FileEntry extends BaseEntry { type: "file" filepath: string nsloc?: number diff --git a/src/pages/AuditScope/AuditScope.module.scss b/src/pages/AuditScope/AuditScope.module.scss index c623df43..b21facaa 100644 --- a/src/pages/AuditScope/AuditScope.module.scss +++ b/src/pages/AuditScope/AuditScope.module.scss @@ -109,3 +109,16 @@ } } } + +.nslocDiff { + color: #ffa837 !important; +} + +.fileAdded { + color: #91e467 !important; +} + +.fileRemoved { + color: #ef3131 !important; + opacity: 1 !important; +} diff --git a/src/pages/AuditScope/RepositoryContractsSelector.tsx b/src/pages/AuditScope/RepositoryContractsSelector.tsx index d834b243..124bd4b6 100644 --- a/src/pages/AuditScope/RepositoryContractsSelector.tsx +++ b/src/pages/AuditScope/RepositoryContractsSelector.tsx @@ -1,9 +1,20 @@ import { useCallback, useMemo, useState } from "react" -import { FaCaretDown, FaCaretRight, FaCheckCircle, FaFile, FaFolder, FaRegFile } from "react-icons/fa" +import { + FaCaretDown, + FaCaretRight, + FaCheckCircle, + FaDotCircle, + FaFile, + FaFolder, + FaMinusCircle, + FaPlusCircle, + FaRegDotCircle, + FaRegFile, +} from "react-icons/fa" import cx from "classnames" import { Column, Row } from "../../components/Layout" import { Text } from "../../components/Text" -import { Entry, getAllTreePaths, RootDirectory } from "../../hooks/api/scope/useRepositoryContracts" +import { Entry, FileEntry, getAllTreePaths, RootDirectory } from "../../hooks/api/scope/useRepositoryContracts" import styles from "./AuditScope.module.scss" import { Button } from "../../components/Button" @@ -34,6 +45,27 @@ type TreeEntryProps = { } ) +const FileIcon: React.FC<{ + entry: FileEntry + selected: boolean + initialScope?: { nSLOC?: number; selected: boolean } +}> = ({ entry, selected, initialScope }) => { + if (!selected && !initialScope?.selected) return null + + if (selected) { + if (initialScope?.selected) { + if (entry.nsloc !== initialScope.nSLOC) { + return + } + return + } else { + return + } + } + + return +} + export const TreeEntry: React.FC = ({ name, tree, @@ -90,10 +122,7 @@ export const TreeEntry: React.FC = ({ diffWithInitialScope === 0 ? "-" : diffWithInitialScope })`} ) : null} - - - - + From aa5f6b34fad47ff8017b996e1a902f2752e96051 Mon Sep 17 00:00:00 2001 From: Fran Rimoldi Date: Mon, 14 Aug 2023 10:01:01 -0300 Subject: [PATCH 08/80] feat: display diff with original scope --- src/components/Text/Text.module.scss | 4 ++ src/components/Text/Text.tsx | 2 +- src/pages/AuditScope/AuditScope.tsx | 37 ++++++++++++++----- .../RepositoryContractsSelector.tsx | 24 +++++++++--- 4 files changed, 51 insertions(+), 16 deletions(-) diff --git a/src/components/Text/Text.module.scss b/src/components/Text/Text.module.scss index 9c818fb5..fedbd966 100644 --- a/src/components/Text/Text.module.scss +++ b/src/components/Text/Text.module.scss @@ -7,6 +7,10 @@ color: red; } + &.info { + color: #ffa837; + } + &.success { color: #58c322; } diff --git a/src/components/Text/Text.tsx b/src/components/Text/Text.tsx index e8dc9475..5fb02ea0 100644 --- a/src/components/Text/Text.tsx +++ b/src/components/Text/Text.tsx @@ -5,7 +5,7 @@ import styles from "./Text.module.scss" type TextSize = "tiny" | "small" | "normal" | "large" | "extra-large" -type TextVariant = "normal" | "primary" | "secondary" | "alternate" | "mono" | "warning" | "success" +type TextVariant = "normal" | "primary" | "secondary" | "alternate" | "mono" | "warning" | "success" | "info" type TextAlignment = "center" | "left" | "right" | "justify" diff --git a/src/pages/AuditScope/AuditScope.tsx b/src/pages/AuditScope/AuditScope.tsx index 8765a13b..7ec4c28e 100644 --- a/src/pages/AuditScope/AuditScope.tsx +++ b/src/pages/AuditScope/AuditScope.tsx @@ -299,21 +299,40 @@ export const AuditScope = () => {   {s.repoName} - - - Selected files: {s.files.filter((f) => f.selected).length} + + + Original Scope from Quote - - nSLOC: {s.files.reduce((t, f) => (t += f.selected ? f.nSLOC ?? 0 : 0), 0)} + + + Files: {s.initialScope?.files.filter((f) => f.selected).length} + + + nSLOC: {initialNSLOC} + + + + + + Currently Selected Scope - + + + Files: {s.files.filter((f) => f.selected).length} + + + nSLOC: {s.files.reduce((t, f) => (t += f.selected ? f.nSLOC ?? 0 : 0), 0)} + + + + {differenceNSLOC ? ( - Difference with initial scoping: + Changes from Original Scope: - - {differenceNSLOC} nSLOC + 0 ? "success" : "info"}> + {`${differenceNSLOC > 0 ? "+" : ""}${differenceNSLOC}`} nSLOC ) : null} diff --git a/src/pages/AuditScope/RepositoryContractsSelector.tsx b/src/pages/AuditScope/RepositoryContractsSelector.tsx index 124bd4b6..56934778 100644 --- a/src/pages/AuditScope/RepositoryContractsSelector.tsx +++ b/src/pages/AuditScope/RepositoryContractsSelector.tsx @@ -50,7 +50,7 @@ const FileIcon: React.FC<{ selected: boolean initialScope?: { nSLOC?: number; selected: boolean } }> = ({ entry, selected, initialScope }) => { - if (!selected && !initialScope?.selected) return null + if (!selected && !initialScope?.selected) return if (selected) { if (initialScope?.selected) { @@ -98,7 +98,7 @@ export const TreeEntry: React.FC = ({ if (tree.type === "file") { const selected = selectedPaths.includes(parentPath !== "" ? `${parentPath}/${tree.name}` : tree.name) - const initialScopeFile = initialScope?.files.find((f) => f.filePath === tree.filepath && f.selected) + const initialScopeFile = initialScope?.files.find((f) => f.filePath === tree.filepath) const diffWithInitialScope = initialScopeFile && initialScopeFile.nSLOC && tree.nsloc && tree.nsloc - initialScopeFile.nSLOC @@ -110,7 +110,20 @@ export const TreeEntry: React.FC = ({ {tree.name} - + + {initialScopeFile?.nSLOC ?? "NA"} + + + {tree.nsloc} + + {`${diffWithInitialScope && diffWithInitialScope > 0 ? "+" : ""}${ + diffWithInitialScope === undefined || diffWithInitialScope === 0 ? "NA" : diffWithInitialScope + }`} + {/* {`${!initialScopeFile ? "+ " : ""} ${tree.nsloc}`} {diffWithInitialScope !== undefined ? ( @@ -121,7 +134,7 @@ export const TreeEntry: React.FC = ({ >{`(${diffWithInitialScope > 0 ? "+" : ""}${ diffWithInitialScope === 0 ? "-" : diffWithInitialScope })`} - ) : null} + ) : null} */} @@ -222,9 +235,8 @@ export const RepositoryContractsSelector: React.FC = ({ - nSLOC + Original | Current | Diff - (Diff.) From 0cf7fcecb5ac1fb98a542f25869b475dd361a52a Mon Sep 17 00:00:00 2001 From: Fran Rimoldi Date: Tue, 15 Aug 2023 10:35:24 -0300 Subject: [PATCH 09/80] feat: legend --- src/pages/AuditScope/AuditScope.module.scss | 4 + src/pages/AuditScope/AuditScope.tsx | 133 +++++++++++------- .../RepositoryContractsSelector.tsx | 12 -- 3 files changed, 84 insertions(+), 65 deletions(-) diff --git a/src/pages/AuditScope/AuditScope.module.scss b/src/pages/AuditScope/AuditScope.module.scss index b21facaa..a84bf468 100644 --- a/src/pages/AuditScope/AuditScope.module.scss +++ b/src/pages/AuditScope/AuditScope.module.scss @@ -122,3 +122,7 @@ color: #ef3131 !important; opacity: 1 !important; } + +.fileSame { + color: $primary-purple !important; +} diff --git a/src/pages/AuditScope/AuditScope.tsx b/src/pages/AuditScope/AuditScope.tsx index 7ec4c28e..388d97a5 100644 --- a/src/pages/AuditScope/AuditScope.tsx +++ b/src/pages/AuditScope/AuditScope.tsx @@ -1,5 +1,5 @@ import { useCallback, useEffect, useMemo, useState } from "react" -import { FaGithub, FaTrashAlt } from "react-icons/fa" +import { FaCheckCircle, FaGithub, FaMinusCircle, FaPlusCircle, FaRegDotCircle, FaTrashAlt } from "react-icons/fa" import { Box } from "../../components/Box" import { Button } from "../../components/Button" import { Input } from "../../components/Input" @@ -231,60 +231,87 @@ export const AuditScope = () => { {scope && scope.length > 0 ? ( - - - Branches & commits - - Select branch and commit hash - - - - Repo - - {scope?.map((s) => ( - - ))} - - - - Branch - - {scope?.map((s) => ( - - ))} - - - - Commit hash + <> + + + Branches & commits + + Select branch and commit hash + + + + Repo + + {scope?.map((s) => ( + + ))} + + + + Branch + + {scope?.map((s) => ( + + ))} + + + + Commit hash + + {scope?.map((s) => ( + + ))} + + + Remove + {scope?.map((s) => ( + + ))} + + + + + + + + Legend + + + + + File selected in both original and current scope. No change in nSLOC. - {scope?.map((s) => ( - - ))} - - - Remove - {scope?.map((s) => ( - - ))} - - + + + + File selected in both original and current scope. nSLOC changed. + + + + File added + + + + File removed + + - - + + ) : null} {scope?.map((s) => { const selectedNSLOC = s.files.reduce((t, f) => (t += f.selected ? f.nSLOC ?? 0 : 0), 0) diff --git a/src/pages/AuditScope/RepositoryContractsSelector.tsx b/src/pages/AuditScope/RepositoryContractsSelector.tsx index 56934778..bc85a143 100644 --- a/src/pages/AuditScope/RepositoryContractsSelector.tsx +++ b/src/pages/AuditScope/RepositoryContractsSelector.tsx @@ -123,18 +123,6 @@ export const TreeEntry: React.FC = ({ >{`${diffWithInitialScope && diffWithInitialScope > 0 ? "+" : ""}${ diffWithInitialScope === undefined || diffWithInitialScope === 0 ? "NA" : diffWithInitialScope }`} - {/* - {`${!initialScopeFile ? "+ " : ""} ${tree.nsloc}`} - - {diffWithInitialScope !== undefined ? ( - 0 })} - >{`(${diffWithInitialScope > 0 ? "+" : ""}${ - diffWithInitialScope === 0 ? "-" : diffWithInitialScope - })`} - ) : null} */} From 0f2cd9392cceb3d6d1832291b487d55928cbd75b Mon Sep 17 00:00:00 2001 From: Fran Rimoldi Date: Tue, 15 Aug 2023 19:44:12 -0300 Subject: [PATCH 10/80] feat: admin scope --- .../RepositoryContractsSelector.tsx | 38 +++++++++++-------- src/pages/admin/AdminScope/AdminScope.tsx | 16 +------- 2 files changed, 23 insertions(+), 31 deletions(-) diff --git a/src/pages/AuditScope/RepositoryContractsSelector.tsx b/src/pages/AuditScope/RepositoryContractsSelector.tsx index bc85a143..ff2eb27d 100644 --- a/src/pages/AuditScope/RepositoryContractsSelector.tsx +++ b/src/pages/AuditScope/RepositoryContractsSelector.tsx @@ -110,19 +110,23 @@ export const TreeEntry: React.FC = ({ {tree.name} - - {initialScopeFile?.nSLOC ?? "NA"} - - - {tree.nsloc} - - {`${diffWithInitialScope && diffWithInitialScope > 0 ? "+" : ""}${ - diffWithInitialScope === undefined || diffWithInitialScope === 0 ? "NA" : diffWithInitialScope - }`} + {initialScope ? ( + <> + + {initialScopeFile?.nSLOC ?? "NA"} + + + {tree.nsloc} + + {`${diffWithInitialScope && diffWithInitialScope > 0 ? "+" : ""}${ + diffWithInitialScope === undefined || diffWithInitialScope === 0 ? "NA" : diffWithInitialScope + }`} + + ) : null} @@ -222,9 +226,11 @@ export const RepositoryContractsSelector: React.FC = ({ Files - - Original | Current | Diff - + {initialScope ? ( + + Original | Current | Diff + + ) : null} diff --git a/src/pages/admin/AdminScope/AdminScope.tsx b/src/pages/admin/AdminScope/AdminScope.tsx index a305d1b6..a7809c5c 100644 --- a/src/pages/admin/AdminScope/AdminScope.tsx +++ b/src/pages/admin/AdminScope/AdminScope.tsx @@ -144,21 +144,7 @@ export const AdminScope = () => { {report ? ( - - nSLOC: - {report.nSLOC} - - - - - - - + ) : ( )} From 2d2cbb1fb445bf5acd772c61fda715e00b209b09 Mon Sep 17 00:00:00 2001 From: Fran Rimoldi Date: Wed, 16 Aug 2023 18:32:10 -0300 Subject: [PATCH 13/80] fix: update scope performance --- src/hooks/api/scope/useUpdateScope.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/hooks/api/scope/useUpdateScope.ts b/src/hooks/api/scope/useUpdateScope.ts index ab7f2176..97a205ac 100644 --- a/src/hooks/api/scope/useUpdateScope.ts +++ b/src/hooks/api/scope/useUpdateScope.ts @@ -38,8 +38,6 @@ export const useUpdateScope = () => { }, { onMutate: async (params) => { - await queryClient.invalidateQueries(scopeQueryKey(params.protocolDashboardID)) - const previousScope = queryClient.getQueryData(scopeQueryKey(params.protocolDashboardID)) queryClient.setQueryData(scopeQueryKey(params.protocolDashboardID), (previous) => { From a39e439c9cbc3762eb2fea83a6316c2b707cf62d Mon Sep 17 00:00:00 2001 From: Fran Rimoldi Date: Thu, 17 Aug 2023 09:11:17 -0300 Subject: [PATCH 14/80] fix: make twitter field optional --- src/pages/admin/AdminContestsList/CreateContestModal.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/pages/admin/AdminContestsList/CreateContestModal.tsx b/src/pages/admin/AdminContestsList/CreateContestModal.tsx index 9ab7ac16..a6d62cb8 100644 --- a/src/pages/admin/AdminContestsList/CreateContestModal.tsx +++ b/src/pages/admin/AdminContestsList/CreateContestModal.tsx @@ -136,7 +136,6 @@ export const CreateContestModal: React.FC = ({ onClose }) => { if (protocolName === "") return false if (protocolLogoURL === "" && !protocol?.logoURL) return false if (protocolWebsite === "" && !protocol?.website) return false - if (protocolTwitter === "" && !protocol?.twitter) return false if (protocolGithubTeam === "" && !protocol?.githubTeam) return false if (contestTitle === "") return false @@ -161,12 +160,10 @@ export const CreateContestModal: React.FC = ({ onClose }) => { contestTitle, contestTotalCost, protocol?.logoURL, - protocol?.twitter, protocol?.website, protocol?.githubTeam, protocolLogoURL, protocolName, - protocolTwitter, protocolWebsite, protocolGithubTeam, ]) From d917a6c24dcf0b747d0e9e84c1d5cb8bea67416c Mon Sep 17 00:00:00 2001 From: Fran Rimoldi Date: Thu, 17 Aug 2023 16:31:54 -0300 Subject: [PATCH 15/80] fix: contest pool tbd --- src/AppProtocolDashboard.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/AppProtocolDashboard.tsx b/src/AppProtocolDashboard.tsx index b47c4b48..08aaf0be 100644 --- a/src/AppProtocolDashboard.tsx +++ b/src/AppProtocolDashboard.tsx @@ -96,7 +96,9 @@ const AppProtocolDashboard = () => { Contest Prize Pool - {`${commify(protocolDashboard.contest.prizePool)} USDC`} + {`${ + protocolDashboard.contest.prizePool ? commify(protocolDashboard.contest.prizePool) : "TBD" + } USDC`} From 88775f0cc87223e24fccb1339aaffda032a271d7 Mon Sep 17 00:00:00 2001 From: Fran Rimoldi Date: Thu, 17 Aug 2023 17:00:52 -0300 Subject: [PATCH 16/80] fix: lead senior fixed pay tbd --- src/AppProtocolDashboard.tsx | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/AppProtocolDashboard.tsx b/src/AppProtocolDashboard.tsx index 08aaf0be..6f735a30 100644 --- a/src/AppProtocolDashboard.tsx +++ b/src/AppProtocolDashboard.tsx @@ -96,9 +96,11 @@ const AppProtocolDashboard = () => { Contest Prize Pool - {`${ - protocolDashboard.contest.prizePool ? commify(protocolDashboard.contest.prizePool) : "TBD" - } USDC`} + + {protocolDashboard.contest.prizePool + ? ` ${commify(protocolDashboard.contest.prizePool)} USDC` + : "TBD"} + @@ -106,9 +108,11 @@ const AppProtocolDashboard = () => { Lead Senior Watson Fixed Pay - {`${commify( - protocolDashboard.contest.leadSeniorAuditorFixedPay - )} USDC`} + + {protocolDashboard.contest.leadSeniorAuditorFixedPay + ? `${commify(protocolDashboard.contest.leadSeniorAuditorFixedPay)} USDC` + : "TBD"} + From 0d22825f313bb7fd5f27966a2d0ad5dbad49d0de Mon Sep 17 00:00:00 2001 From: Fran Rimoldi Date: Fri, 18 Aug 2023 19:04:41 -0300 Subject: [PATCH 17/80] feat: diplay nsloc even if there is no initial scope --- .../RepositoryContractsSelector.tsx | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/pages/AuditScope/RepositoryContractsSelector.tsx b/src/pages/AuditScope/RepositoryContractsSelector.tsx index ff2eb27d..75f168c4 100644 --- a/src/pages/AuditScope/RepositoryContractsSelector.tsx +++ b/src/pages/AuditScope/RepositoryContractsSelector.tsx @@ -111,21 +111,21 @@ export const TreeEntry: React.FC = ({ {initialScope ? ( - <> - - {initialScopeFile?.nSLOC ?? "NA"} - - - {tree.nsloc} - - {`${diffWithInitialScope && diffWithInitialScope > 0 ? "+" : ""}${ - diffWithInitialScope === undefined || diffWithInitialScope === 0 ? "NA" : diffWithInitialScope - }`} - + + {initialScopeFile?.nSLOC ?? "NA"} + + ) : null} + + {tree.nsloc} + + {initialScope ? ( + {`${diffWithInitialScope && diffWithInitialScope > 0 ? "+" : ""}${ + diffWithInitialScope === undefined || diffWithInitialScope === 0 ? "NA" : diffWithInitialScope + }`} ) : null} From 9c4519353590ab0d434faf18ee9525bfa1c21cc6 Mon Sep 17 00:00:00 2001 From: Fran Rimoldi Date: Fri, 18 Aug 2023 19:11:47 -0300 Subject: [PATCH 18/80] fix: update nsloc labels --- src/pages/AuditScope/AuditScope.tsx | 4 ++-- src/pages/AuditScope/RepositoryContractsSelector.tsx | 8 ++++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/pages/AuditScope/AuditScope.tsx b/src/pages/AuditScope/AuditScope.tsx index 388d97a5..56b97f0c 100644 --- a/src/pages/AuditScope/AuditScope.tsx +++ b/src/pages/AuditScope/AuditScope.tsx @@ -302,11 +302,11 @@ export const AuditScope = () => { - File added + File added to scope - File removed + File removed from scope diff --git a/src/pages/AuditScope/RepositoryContractsSelector.tsx b/src/pages/AuditScope/RepositoryContractsSelector.tsx index 75f168c4..d8d4d638 100644 --- a/src/pages/AuditScope/RepositoryContractsSelector.tsx +++ b/src/pages/AuditScope/RepositoryContractsSelector.tsx @@ -228,9 +228,13 @@ export const RepositoryContractsSelector: React.FC = ({ {initialScope ? ( - Original | Current | Diff + Original nSLOC | Current nSLOC | Diff - ) : null} + ) : ( + + nSLOC + + )} From e8e86232874a06f65d47d2b741ae92aa3a21806b Mon Sep 17 00:00:00 2001 From: Fran Rimoldi Date: Mon, 21 Aug 2023 20:57:18 -0300 Subject: [PATCH 19/80] refactor create contest form --- .../AdminContestsList/CreateContestForm.tsx | 388 ++++++++++++++++++ .../AdminContestsList/CreateContestModal.tsx | 361 +--------------- 2 files changed, 393 insertions(+), 356 deletions(-) create mode 100644 src/pages/admin/AdminContestsList/CreateContestForm.tsx diff --git a/src/pages/admin/AdminContestsList/CreateContestForm.tsx b/src/pages/admin/AdminContestsList/CreateContestForm.tsx new file mode 100644 index 00000000..dc57dcca --- /dev/null +++ b/src/pages/admin/AdminContestsList/CreateContestForm.tsx @@ -0,0 +1,388 @@ +import { useCallback, useEffect, useMemo, useState } from "react" +import { FaChrome, FaGithub, FaTwitter } from "react-icons/fa" +import { useDebounce } from "use-debounce" +import { BigNumber, ethers } from "ethers" +import { DateTime } from "luxon" + +import { Input } from "../../../components/Input" +import { Column, Row } from "../../../components/Layout" +import { Title } from "../../../components/Title" +import { useAdminProtocol } from "../../../hooks/api/admin/useAdminProtocol" +import { Field } from "../../Claim/Field" +import { useAdminTwitterAccount } from "../../../hooks/api/admin/useTwitterAccount" +import { useAdminContestVariables } from "../../../hooks/api/admin/useAdminContestVariables" +import { commify } from "../../../utils/units" +import { Text } from "../../../components/Text" +import TokenInput from "../../../components/TokenInput/TokenInput" +import { Button } from "../../../components/Button" + +type ContestValues = { + protocol: { + id?: number + name: string + twitter: string + githubTeam: string + website: string + logoUrl: string + } + contest: { + title: string + shortDescription: string + nSLOC: string + startDate: DateTime + endDate: DateTime + auditRewards: number + judgingPrizePool: number + leadJudgeFixedPay: number + fullPayment: number + } +} + +type Props = { + onSubmit: (values: ContestValues) => void + onDirtyChange: (dirty: boolean) => void + submitLabel: string +} + +const DATE_FORMAT = "yyyy-MM-dd" + +export const CreateContestForm: React.FC = ({ onSubmit, onDirtyChange, submitLabel }) => { + const [protocolName, setProtocolName] = useState("") + const [debouncedProtocolName] = useDebounce(protocolName, 300) + + const { + data: protocol, + isError: protocolNotFound, + isLoading: protocolLoading, + } = useAdminProtocol(debouncedProtocolName) + + const [protocolTwitter, setProtocolTwitter] = useState(protocol?.twitter ?? "") + const [protocolGithubTeam, setProtocolGithubTeam] = useState(protocol?.githubTeam ?? "") + const [protocolWebsite, setProtocolWebsite] = useState(protocol?.website ?? "") + const [protocolLogoURL, setProtocolLogoURL] = useState(protocol?.logoURL ?? "") + + const [debouncedProtocolTwitter] = useDebounce(protocolTwitter, 300) + const { data: twitterAccount } = useAdminTwitterAccount(debouncedProtocolTwitter) + + const [contestTitle, setContestTitle] = useState("") + const [contestShortDescription, setShortDescription] = useState("") + const [contestNSLOC, setContestNSLOC] = useState("") + const [debouncedContestNSLOC] = useDebounce(contestNSLOC, 300) + const [contestStartDate, setContestStartDate] = useState("") + const [contestAuditLength, setContestAuditLength] = useState("") + const [contestAuditRewards, setContestAuditRewards] = useState(BigNumber.from(0)) + const [contestJudgingPrizePool, setContestJudgingPrizePool] = useState(BigNumber.from(0)) + const [contestLeadJudgeFixedPay, setContestLeadJudgeFixedPay] = useState(BigNumber.from(0)) + const [contestTotalCost, setContestTotalCost] = useState(BigNumber.from(0)) + const [initialAuditContestRewards, setInitialAuditContestRewards] = useState(BigNumber.from(0)) + const [initialJudgingPrizePool, setInitialJudgingPrizePool] = useState(BigNumber.from(0)) + const [initialLeadJudgeFixedPay, setInitialLeadJudgeFixedPay] = useState(BigNumber.from(0)) + const [initialTotalCost, setInitialTotalCost] = useState(BigNumber.from(0)) + + const [startDateError, setStartDateError] = useState() + const [shortDescriptionError, setShortDescriptionError] = useState() + + const displayProtocolInfo = !!protocol || protocolNotFound || protocolLoading + + const { data: contestVariables, isSuccess: contestVariablesSuccess } = useAdminContestVariables( + parseInt(debouncedContestNSLOC) + ) + + useEffect(() => { + if (contestVariablesSuccess) { + setContestAuditLength(`${contestVariables.length}`) + + setInitialAuditContestRewards(ethers.utils.parseUnits(`${contestVariables.auditContestRewards}`, 6)) + setInitialJudgingPrizePool(ethers.utils.parseUnits(`${contestVariables.judgingPrizePool}`, 6)) + setInitialLeadJudgeFixedPay(ethers.utils.parseUnits(`${contestVariables.leadJudgeFixedPay}`, 6)) + setInitialTotalCost(ethers.utils.parseUnits(`${contestVariables.fullPayment}`, 6)) + } else { + setContestAuditLength("") + setInitialAuditContestRewards(BigNumber.from(0)) + setInitialJudgingPrizePool(BigNumber.from(0)) + setInitialLeadJudgeFixedPay(BigNumber.from(0)) + setInitialTotalCost(BigNumber.from(0)) + } + }, [contestVariablesSuccess, setContestAuditLength, contestVariables]) + + useEffect(() => { + if (protocol?.name) { + setProtocolName(protocol?.name) + } + }, [protocol]) + + useEffect(() => { + if (twitterAccount?.profilePictureUrl) { + setProtocolLogoURL(twitterAccount.profilePictureUrl) + } else { + setProtocolLogoURL("") + } + }, [twitterAccount]) + + useEffect(() => { + if (contestStartDate === "") { + setStartDateError(undefined) + return + } + + const startDate = DateTime.fromFormat(contestStartDate, DATE_FORMAT) + + if (!startDate.isValid) { + setStartDateError(`Invalid date. Must be format ${DATE_FORMAT}`) + return + } + + if (startDate < DateTime.now()) { + setStartDateError("Start date cannot be in the past.") + return + } + + setStartDateError(undefined) + }, [contestStartDate, contestAuditLength]) + + const sherlockFee = useMemo(() => { + const fee = contestTotalCost + ?.sub(contestAuditRewards ?? 0) + .sub(contestJudgingPrizePool ?? 0) + .sub(contestLeadJudgeFixedPay ?? 0) + + return commify(parseInt(ethers.utils.formatUnits(fee ?? 0, 6))) + }, [contestTotalCost, contestAuditRewards, contestJudgingPrizePool, contestLeadJudgeFixedPay]) + + const canSubmit = useMemo(() => { + if (protocolName === "") return false + if (protocolLogoURL === "" && !protocol?.logoURL) return false + if (protocolWebsite === "" && !protocol?.website) return false + if (protocolGithubTeam === "" && !protocol?.githubTeam) return false + + if (contestTitle === "") return false + if (contestShortDescription.length < 100 || contestShortDescription.length > 200) return false + + if (contestAuditLength === "") return false + + const startDate = DateTime.fromFormat(contestStartDate, DATE_FORMAT) + + if (!startDate.isValid) return false + if (startDate < DateTime.now()) return false + + if (contestAuditRewards?.eq(BigNumber.from(0))) return false + if (contestTotalCost?.eq(BigNumber.from(0))) return false + + return true + }, [ + contestAuditLength, + contestAuditRewards, + contestShortDescription.length, + contestStartDate, + contestTitle, + contestTotalCost, + protocol?.logoURL, + protocol?.website, + protocol?.githubTeam, + protocolLogoURL, + protocolName, + protocolWebsite, + protocolGithubTeam, + ]) + + useEffect(() => { + onDirtyChange( + (protocolName !== "" || + contestTitle !== "" || + contestShortDescription !== "" || + contestNSLOC !== "" || + contestStartDate !== "" || + contestAuditLength !== "" || + contestAuditRewards?.gt(BigNumber.from(0)) || + contestJudgingPrizePool?.gt(BigNumber.from(0)) || + contestLeadJudgeFixedPay?.gt(BigNumber.from(0)) || + contestTotalCost?.gt(BigNumber.from(0))) ?? + false + ) + }, [ + contestAuditLength, + contestAuditRewards, + contestJudgingPrizePool, + contestLeadJudgeFixedPay, + contestNSLOC, + contestShortDescription, + contestStartDate, + contestTitle, + contestTotalCost, + onDirtyChange, + protocolName, + ]) + + const handleUpdateShortDescription = useCallback((value: string) => { + setShortDescription(value) + + if (value === "") { + setShortDescriptionError(undefined) + } else if (value.length < 100) { + setShortDescriptionError("Too short. Must be between 100 and 200 characters.") + } else if (value.length > 200) { + setShortDescriptionError("Too long. Must be between 100 and 200 characters.") + } else { + setShortDescriptionError(undefined) + } + }, []) + + const handleSubmit = useCallback(() => { + const startDate = DateTime.fromFormat(contestStartDate, DATE_FORMAT, { zone: "utc" }).set({ + hour: 15, + minute: 0, + second: 0, + millisecond: 0, + }) + const endDate = startDate + .plus({ hours: 24 * parseInt(contestAuditLength) }) + .set({ hour: 15, minute: 0, second: 0, millisecond: 0 }) + onSubmit({ + protocol: { + id: protocol?.id, + name: protocolName, + githubTeam: protocolGithubTeam, + twitter: protocolTwitter, + website: protocolWebsite, + logoUrl: protocolLogoURL, + }, + contest: { + title: contestTitle, + shortDescription: contestShortDescription, + nSLOC: contestNSLOC, + startDate, + endDate, + auditRewards: parseInt(ethers.utils.formatUnits(contestAuditRewards ?? 0, 6)), + judgingPrizePool: parseInt(ethers.utils.formatUnits(contestJudgingPrizePool ?? 0, 6)), + leadJudgeFixedPay: parseInt(ethers.utils.formatUnits(contestLeadJudgeFixedPay ?? 0, 6)), + fullPayment: parseInt(ethers.utils.formatUnits(contestTotalCost ?? 0, 6)), + }, + }) + }, [ + contestAuditLength, + contestAuditRewards, + contestJudgingPrizePool, + contestLeadJudgeFixedPay, + contestNSLOC, + contestShortDescription, + contestStartDate, + contestTitle, + contestTotalCost, + onSubmit, + protocol?.id, + protocolGithubTeam, + protocolLogoURL, + protocolName, + protocolTwitter, + protocolWebsite, + ]) + + return ( + + + PROTOCOL + + + + {displayProtocolInfo && ( + <> + + + GitHub + + } + > + + + + + Twitter + + } + > + + + + + Website + + } + > + + + + + + {(protocol?.logoURL || protocolLogoURL) && ( + logo preview + )} + + + + )} + +
+ + CONTEST + + + + + + + + + + + + + + + + + + + + + + + + + + + + {`Admin Fee: ${sherlockFee} USDC`} + +
+ +
+ ) +} diff --git a/src/pages/admin/AdminContestsList/CreateContestModal.tsx b/src/pages/admin/AdminContestsList/CreateContestModal.tsx index a6d62cb8..305b2f4b 100644 --- a/src/pages/admin/AdminContestsList/CreateContestModal.tsx +++ b/src/pages/admin/AdminContestsList/CreateContestModal.tsx @@ -1,264 +1,26 @@ -import { BigNumber, ethers } from "ethers" -import { DateTime } from "luxon" -import { useCallback, useEffect, useMemo, useState } from "react" -import { FaChrome, FaTwitter, FaGithub } from "react-icons/fa" -import { useDebounce } from "use-debounce" +import { useCallback, useEffect, useState } from "react" import { Button } from "../../../components/Button" -import { Input } from "../../../components/Input" import { Column, Row } from "../../../components/Layout" import LoadingContainer from "../../../components/LoadingContainer/LoadingContainer" import Modal, { Props as ModalProps } from "../../../components/Modal/Modal" import { Text } from "../../../components/Text" import { Title } from "../../../components/Title" -import TokenInput from "../../../components/TokenInput/TokenInput" import { useAdminCreateContest } from "../../../hooks/api/admin/useAdminCreateContest" -import { useAdminProtocol } from "../../../hooks/api/admin/useAdminProtocol" -import { commify } from "../../../utils/units" -import { Field } from "../../Claim/Field" import { ErrorModal } from "../../../pages/ContestDetails/ErrorModal" -import { useAdminTwitterAccount } from "../../../hooks/api/admin/useTwitterAccount" -import { useAdminContestVariables } from "../../../hooks/api/admin/useAdminContestVariables" +import { CreateContestForm } from "./CreateContestForm" type Props = ModalProps & {} -const DATE_FORMAT = "yyyy-MM-dd" - export const CreateContestModal: React.FC = ({ onClose }) => { - const [protocolName, setProtocolName] = useState("") - const [debouncedProtocolName] = useDebounce(protocolName, 300) - const { - data: protocol, - isError: protocolNotFound, - isLoading: protocolLoading, - } = useAdminProtocol(debouncedProtocolName) + const [formIsDirty, setFormIsDirty] = useState(false) const { createContest, isLoading, isSuccess, error, reset } = useAdminCreateContest() - const [protocolTwitter, setProtocolTwitter] = useState(protocol?.twitter ?? "") - const [protocolGithubTeam, setProtocolGithubTeam] = useState(protocol?.githubTeam ?? "") - const [protocolWebsite, setProtocolWebsite] = useState(protocol?.website ?? "") - const [protocolLogoURL, setProtocolLogoURL] = useState(protocol?.logoURL ?? "") - - const [debouncedProtocolTwitter] = useDebounce(protocolTwitter, 300) - const { data: twitterAccount } = useAdminTwitterAccount(debouncedProtocolTwitter) - - const [contestTitle, setContestTitle] = useState("") - const [contestShortDescription, setShortDescription] = useState("") - const [contestNSLOC, setContestNSLOC] = useState("") - const [debouncedContestNSLOC] = useDebounce(contestNSLOC, 300) - const [contestStartDate, setContestStartDate] = useState("") - const [contestAuditLength, setContestAuditLength] = useState("") - const [contestAuditRewards, setContestAuditRewards] = useState(BigNumber.from(0)) - const [contestJudgingPrizePool, setContestJudgingPrizePool] = useState(BigNumber.from(0)) - const [contestLeadJudgeFixedPay, setContestLeadJudgeFixedPay] = useState(BigNumber.from(0)) - const [contestTotalCost, setContestTotalCost] = useState(BigNumber.from(0)) - const [initialAuditContestRewards, setInitialAuditContestRewards] = useState(BigNumber.from(0)) - const [initialJudgingPrizePool, setInitialJudgingPrizePool] = useState(BigNumber.from(0)) - const [initialLeadJudgeFixedPay, setInitialLeadJudgeFixedPay] = useState(BigNumber.from(0)) - const [initialTotalCost, setInitialTotalCost] = useState(BigNumber.from(0)) - - const [startDateError, setStartDateError] = useState() - const [shortDescriptionError, setShortDescriptionError] = useState() - const [displayModalCloseConfirm, setDisplayModalFormConfirm] = useState(false) - const displayProtocolInfo = !!protocol || protocolNotFound || protocolLoading - - const { data: contestVariables, isSuccess: contestVariablesSuccess } = useAdminContestVariables( - parseInt(debouncedContestNSLOC) - ) - - useEffect(() => { - if (contestVariablesSuccess) { - setContestAuditLength(`${contestVariables.length}`) - - setInitialAuditContestRewards(ethers.utils.parseUnits(`${contestVariables.auditContestRewards}`, 6)) - setInitialJudgingPrizePool(ethers.utils.parseUnits(`${contestVariables.judgingPrizePool}`, 6)) - setInitialLeadJudgeFixedPay(ethers.utils.parseUnits(`${contestVariables.leadJudgeFixedPay}`, 6)) - setInitialTotalCost(ethers.utils.parseUnits(`${contestVariables.fullPayment}`, 6)) - } else { - setContestAuditLength("") - setInitialAuditContestRewards(BigNumber.from(0)) - setInitialJudgingPrizePool(BigNumber.from(0)) - setInitialLeadJudgeFixedPay(BigNumber.from(0)) - setInitialTotalCost(BigNumber.from(0)) - } - }, [contestVariablesSuccess, setContestAuditLength, contestVariables]) - useEffect(() => { if (isSuccess) onClose?.() }, [isSuccess, onClose]) - useEffect(() => { - if (protocol?.name) { - setProtocolName(protocol?.name) - } - }, [protocol]) - - useEffect(() => { - if (twitterAccount?.profilePictureUrl) { - setProtocolLogoURL(twitterAccount.profilePictureUrl) - } else { - setProtocolLogoURL("") - } - }, [twitterAccount]) - - useEffect(() => { - if (contestStartDate === "") { - setStartDateError(undefined) - return - } - - const startDate = DateTime.fromFormat(contestStartDate, DATE_FORMAT) - - if (!startDate.isValid) { - setStartDateError(`Invalid date. Must be format ${DATE_FORMAT}`) - return - } - - if (startDate < DateTime.now()) { - setStartDateError("Start date cannot be in the past.") - return - } - - setStartDateError(undefined) - }, [contestStartDate, contestAuditLength]) - - const sherlockFee = useMemo(() => { - const fee = contestTotalCost - ?.sub(contestAuditRewards ?? 0) - .sub(contestJudgingPrizePool ?? 0) - .sub(contestLeadJudgeFixedPay ?? 0) - - return commify(parseInt(ethers.utils.formatUnits(fee ?? 0, 6))) - }, [contestTotalCost, contestAuditRewards, contestJudgingPrizePool, contestLeadJudgeFixedPay]) - - const canCreateContest = useMemo(() => { - if (protocolName === "") return false - if (protocolLogoURL === "" && !protocol?.logoURL) return false - if (protocolWebsite === "" && !protocol?.website) return false - if (protocolGithubTeam === "" && !protocol?.githubTeam) return false - - if (contestTitle === "") return false - if (contestShortDescription.length < 100 || contestShortDescription.length > 200) return false - - if (contestAuditLength === "") return false - - const startDate = DateTime.fromFormat(contestStartDate, DATE_FORMAT) - - if (!startDate.isValid) return false - if (startDate < DateTime.now()) return false - - if (contestAuditRewards?.eq(BigNumber.from(0))) return false - if (contestTotalCost?.eq(BigNumber.from(0))) return false - - return true - }, [ - contestAuditLength, - contestAuditRewards, - contestShortDescription.length, - contestStartDate, - contestTitle, - contestTotalCost, - protocol?.logoURL, - protocol?.website, - protocol?.githubTeam, - protocolLogoURL, - protocolName, - protocolWebsite, - protocolGithubTeam, - ]) - - const handleUpdateShortDescription = useCallback((value: string) => { - setShortDescription(value) - - if (value === "") { - setShortDescriptionError(undefined) - } else if (value.length < 100) { - setShortDescriptionError("Too short. Must be between 100 and 200 characters.") - } else if (value.length > 200) { - setShortDescriptionError("Too long. Must be between 100 and 200 characters.") - } else { - setShortDescriptionError(undefined) - } - }, []) - - const handleCreateContest = useCallback(() => { - const startDate = DateTime.fromFormat(contestStartDate, DATE_FORMAT, { zone: "utc" }).set({ - hour: 15, - minute: 0, - second: 0, - millisecond: 0, - }) - const endDate = startDate - .plus({ hours: 24 * parseInt(contestAuditLength) }) - .set({ hour: 15, minute: 0, second: 0, millisecond: 0 }) - - createContest({ - protocol: { - id: protocol?.id, - name: protocolName, - githubTeam: protocolGithubTeam || undefined, - website: protocolWebsite || undefined, - twitter: protocolTwitter || undefined, - logoUrl: protocolLogoURL || undefined, - }, - contest: { - title: contestTitle, - shortDescription: contestShortDescription, - nSLOC: contestNSLOC, - startDate, - endDate, - auditRewards: parseInt(ethers.utils.formatUnits(contestAuditRewards ?? 0, 6)), - judgingPrizePool: parseInt(ethers.utils.formatUnits(contestJudgingPrizePool ?? 0, 6)), - leadJudgeFixedPay: parseInt(ethers.utils.formatUnits(contestLeadJudgeFixedPay ?? 0, 6)), - fullPayment: parseInt(ethers.utils.formatUnits(contestTotalCost ?? 0, 6)), - }, - }) - }, [ - contestAuditLength, - contestAuditRewards, - contestJudgingPrizePool, - contestLeadJudgeFixedPay, - contestNSLOC, - contestShortDescription, - contestStartDate, - contestTitle, - contestTotalCost, - createContest, - protocol?.id, - protocolLogoURL, - protocolName, - protocolTwitter, - protocolWebsite, - protocolGithubTeam, - ]) - - const formIsDirty = useMemo( - () => - protocolName !== "" || - contestTitle !== "" || - contestShortDescription !== "" || - contestNSLOC !== "" || - contestStartDate !== "" || - contestAuditLength !== "" || - contestAuditRewards?.gt(BigNumber.from(0)) || - contestJudgingPrizePool?.gt(BigNumber.from(0)) || - contestLeadJudgeFixedPay?.gt(BigNumber.from(0)) || - contestTotalCost?.gt(BigNumber.from(0)), - [ - contestAuditLength, - contestAuditRewards, - contestJudgingPrizePool, - contestLeadJudgeFixedPay, - contestNSLOC, - contestShortDescription, - contestStartDate, - contestTitle, - contestTotalCost, - protocolName, - ] - ) - const handleModalClose = useCallback(() => { if (formIsDirty) { setDisplayModalFormConfirm(true) @@ -294,123 +56,10 @@ export const CreateContestModal: React.FC = ({ onClose }) => { )} - + New contest - - PROTOCOL - - - - {displayProtocolInfo && ( - <> - - - GitHub - - } - > - - - - - Twitter - - } - > - - - - - Website - - } - > - - - - - - {(protocol?.logoURL || protocolLogoURL) && ( - logo preview - )} - - - - )} - -
- - CONTEST - - - - - - - - - - - - - - - - - - - - - - - - - - - - {`Admin Fee: ${sherlockFee} USDC`} - -
- +
{error && reset()} />} From c2dbb33477bd265cdbe5d4eff046d7d98d161ace Mon Sep 17 00:00:00 2001 From: Fran Rimoldi Date: Tue, 22 Aug 2023 20:35:59 -0300 Subject: [PATCH 20/80] feat: re-use contest form for updates --- src/hooks/api/admin/useAdminContests.ts | 22 ++- .../AdminContestsListActive.tsx | 17 +- .../AdminContestsList/CreateContestForm.tsx | 182 ++++++++++-------- .../AdminContestsList/UpdateContestModal.tsx | 76 ++++++++ 4 files changed, 216 insertions(+), 81 deletions(-) create mode 100644 src/pages/admin/AdminContestsList/UpdateContestModal.tsx diff --git a/src/hooks/api/admin/useAdminContests.ts b/src/hooks/api/admin/useAdminContests.ts index 8ee32661..58541a1d 100644 --- a/src/hooks/api/admin/useAdminContests.ts +++ b/src/hooks/api/admin/useAdminContests.ts @@ -8,10 +8,11 @@ export type ContestStatus = "CREATED" | "RUNNING" | "JUDGING" | "FINISHED" | "ES export type ContestsListItem = { id: number title: string + shortDescription: string logoURL: string status: ContestStatus initialPayment: boolean - fullPayment: boolean + fullPaymentComplete: boolean adminUpcomingApproved: boolean adminStartApproved: boolean dashboardID?: string @@ -23,11 +24,17 @@ export type ContestsListItem = { leadSeniorSelectionMessageSentAt: number leadSeniorConfirmationMessage: string auditReport?: string + linesOfCode?: string + rewards: number + judgingPrizePool: number + leadJudgeFixedPay: number + fullPayment: number } type GetAdminContestsResponse = { id: number title: string + short_description: string logo_url: string status: ContestStatus initial_payment_complete: boolean @@ -43,6 +50,11 @@ type GetAdminContestsResponse = { senior_selection_message_sent_at: number senior_confirmed_message: string audit_report?: string + lines_of_code?: string + audit_rewards: number + judging_prize_pool: number + lead_judge_fixed_pay: number + full_payment: number }[] export type ContestListStatus = "active" | "finished" @@ -55,10 +67,11 @@ export const useAdminContests = (status: ContestListStatus) => return data.map((d) => ({ id: d.id, title: d.title, + shortDescription: d.short_description, logoURL: d.logo_url, status: d.status, initialPayment: d.initial_payment_complete, - fullPayment: d.full_payment_complete, + fullPaymentComplete: d.full_payment_complete, adminUpcomingApproved: d.admin_upcoming_approved, adminStartApproved: d.admin_start_approved, dashboardID: d.dashboard_id, @@ -70,5 +83,10 @@ export const useAdminContests = (status: ContestListStatus) => leadSeniorSelectionMessageSentAt: d.senior_selection_message_sent_at, leadSeniorConfirmationMessage: d.senior_confirmed_message, auditReport: d.audit_report, + linesOfCode: d.lines_of_code, + rewards: d.audit_rewards, + judgingPrizePool: d.judging_prize_pool, + leadJudgeFixedPay: d.lead_judge_fixed_pay, + fullPayment: d.full_payment, })) }) diff --git a/src/pages/admin/AdminContestsList/AdminContestsListActive.tsx b/src/pages/admin/AdminContestsList/AdminContestsListActive.tsx index f852801e..81271478 100644 --- a/src/pages/admin/AdminContestsList/AdminContestsListActive.tsx +++ b/src/pages/admin/AdminContestsList/AdminContestsListActive.tsx @@ -14,6 +14,7 @@ import styles from "./AdminContestsList.module.scss" import { ConfirmContestActionModal } from "./ConfirmContestActionModal" import { CreateContestModal } from "./CreateContestModal" import { ContestScopeModal } from "./ContestScopeModal" +import { UpdateContestModal } from "./UpdateContestModal" type ContestLifeCycleStatus = | "WAITING_INITIAL_PAYMENT" @@ -35,7 +36,7 @@ const getCurrentStatus = (contest: ContestsListItem): ContestLifeCycleStatus => if (!contest.leadSeniorAuditorHandle && !contest.leadSeniorSelectionMessageSentAt) return "READY_TO_SELECT_SENIOR" if (!contest.leadSeniorAuditorHandle) return "WAITING_FOR_SENIOR_SELECTION" if (!contest.adminUpcomingApproved) return "READY_TO_PUBLISH" - if (!contest.fullPayment) return "WAITING_ON_FINAL_PAYMENT" + if (!contest.fullPaymentComplete) return "WAITING_ON_FINAL_PAYMENT" if (!contest.submissionReady) return "WAITING_ON_FINALIZE_SUBMISSION" if (!contest.adminStartApproved) return "READY_TO_APPROVE_START" @@ -75,6 +76,7 @@ export const AdminContestsListActive = () => { const { data: contests, isLoading } = useAdminContests("active") const [confirmationModal, setConfirmationModal] = useState() const [createContestModalOpen, setCreateContestModalOpen] = useState(false) + const [updateContestIndex, setUpdateContextIndex] = useState() const [scopeModal, setScopeModal] = useState() const [forceActionRowIndex, setForceActionRowIndex] = useState() @@ -294,7 +296,12 @@ export const AdminContestsListActive = () => { {c.id} - {c.title} + {c.title} setUpdateContextIndex(index)} + /> {c.title} @@ -359,6 +366,12 @@ export const AdminContestsListActive = () => { force={forceActionRowIndex === confirmationModal.contestIndex} /> )} + {updateContestIndex !== undefined && contests && ( + setUpdateContextIndex(undefined)} + contest={contests[updateContestIndex]} + /> + )} {createContestModalOpen && setCreateContestModalOpen(false)} />} {scopeModal && } diff --git a/src/pages/admin/AdminContestsList/CreateContestForm.tsx b/src/pages/admin/AdminContestsList/CreateContestForm.tsx index dc57dcca..580d797d 100644 --- a/src/pages/admin/AdminContestsList/CreateContestForm.tsx +++ b/src/pages/admin/AdminContestsList/CreateContestForm.tsx @@ -15,6 +15,8 @@ import { commify } from "../../../utils/units" import { Text } from "../../../components/Text" import TokenInput from "../../../components/TokenInput/TokenInput" import { Button } from "../../../components/Button" +import { Contest } from "../../../hooks/api/contests" +import { ContestsListItem } from "../../../hooks/api/admin/useAdminContests" type ContestValues = { protocol: { @@ -42,11 +44,12 @@ type Props = { onSubmit: (values: ContestValues) => void onDirtyChange: (dirty: boolean) => void submitLabel: string + contest?: ContestsListItem } const DATE_FORMAT = "yyyy-MM-dd" -export const CreateContestForm: React.FC = ({ onSubmit, onDirtyChange, submitLabel }) => { +export const CreateContestForm: React.FC = ({ onSubmit, onDirtyChange, submitLabel, contest }) => { const [protocolName, setProtocolName] = useState("") const [debouncedProtocolName] = useDebounce(protocolName, 300) @@ -66,7 +69,7 @@ export const CreateContestForm: React.FC = ({ onSubmit, onDirtyChange, su const [contestTitle, setContestTitle] = useState("") const [contestShortDescription, setShortDescription] = useState("") - const [contestNSLOC, setContestNSLOC] = useState("") + const [contestNSLOC, setContestNSLOC] = useState(contest?.linesOfCode ?? "") const [debouncedContestNSLOC] = useDebounce(contestNSLOC, 300) const [contestStartDate, setContestStartDate] = useState("") const [contestAuditLength, setContestAuditLength] = useState("") @@ -89,21 +92,31 @@ export const CreateContestForm: React.FC = ({ onSubmit, onDirtyChange, su ) useEffect(() => { - if (contestVariablesSuccess) { - setContestAuditLength(`${contestVariables.length}`) + if (contest) { + const startDate = DateTime.fromSeconds(contest.startDate) + const endDate = DateTime.fromSeconds(contest.endDate) + const auditLength = endDate.diff(startDate, "days").days + + setContestTitle(contest.title) + setShortDescription(contest.shortDescription) + setContestStartDate(startDate.toFormat(DATE_FORMAT)) + setContestAuditLength(auditLength.toString()) + setInitialAuditContestRewards(ethers.utils.parseUnits(`${contest.rewards}`, 6)) + setInitialJudgingPrizePool(ethers.utils.parseUnits(`${contest.judgingPrizePool}`, 6)) + setInitialLeadJudgeFixedPay(ethers.utils.parseUnits(`${contest.leadJudgeFixedPay}`, 6)) + setInitialTotalCost(ethers.utils.parseUnits(`${contest.fullPayment}`, 6)) + } + }, [contest]) + useEffect(() => { + if (contestVariablesSuccess && !contest) { + setContestAuditLength(`${contestVariables.length}`) setInitialAuditContestRewards(ethers.utils.parseUnits(`${contestVariables.auditContestRewards}`, 6)) setInitialJudgingPrizePool(ethers.utils.parseUnits(`${contestVariables.judgingPrizePool}`, 6)) setInitialLeadJudgeFixedPay(ethers.utils.parseUnits(`${contestVariables.leadJudgeFixedPay}`, 6)) setInitialTotalCost(ethers.utils.parseUnits(`${contestVariables.fullPayment}`, 6)) - } else { - setContestAuditLength("") - setInitialAuditContestRewards(BigNumber.from(0)) - setInitialJudgingPrizePool(BigNumber.from(0)) - setInitialLeadJudgeFixedPay(BigNumber.from(0)) - setInitialTotalCost(BigNumber.from(0)) } - }, [contestVariablesSuccess, setContestAuditLength, contestVariables]) + }, [contestVariablesSuccess, setContestAuditLength, contestVariables, contest]) useEffect(() => { if (protocol?.name) { @@ -150,10 +163,12 @@ export const CreateContestForm: React.FC = ({ onSubmit, onDirtyChange, su }, [contestTotalCost, contestAuditRewards, contestJudgingPrizePool, contestLeadJudgeFixedPay]) const canSubmit = useMemo(() => { - if (protocolName === "") return false - if (protocolLogoURL === "" && !protocol?.logoURL) return false - if (protocolWebsite === "" && !protocol?.website) return false - if (protocolGithubTeam === "" && !protocol?.githubTeam) return false + if (!contest) { + if (protocolName === "") return false + if (protocolLogoURL === "" && !protocol?.logoURL) return false + if (protocolWebsite === "" && !protocol?.website) return false + if (protocolGithubTeam === "" && !protocol?.githubTeam) return false + } if (contestTitle === "") return false if (contestShortDescription.length < 100 || contestShortDescription.length > 200) return false @@ -183,6 +198,7 @@ export const CreateContestForm: React.FC = ({ onSubmit, onDirtyChange, su protocolName, protocolWebsite, protocolGithubTeam, + contest, ]) useEffect(() => { @@ -279,70 +295,82 @@ export const CreateContestForm: React.FC = ({ onSubmit, onDirtyChange, su return ( - - PROTOCOL - - - - {displayProtocolInfo && ( - <> - - - GitHub - - } - > - + {!contest ? ( + <> + + PROTOCOL + + - - - Twitter - - } - > - - - - - Website - - } - > - - - - - - {(protocol?.logoURL || protocolLogoURL) && ( - logo preview + + + GitHub + + } + > + - )} - - - - )} - -
+ + + + Twitter + + } + > + + + + + Website + + } + > + + + + + + {(protocol?.logoURL || protocolLogoURL) && ( + logo preview + )} + + + + )} + +
+ + ) : null} CONTEST diff --git a/src/pages/admin/AdminContestsList/UpdateContestModal.tsx b/src/pages/admin/AdminContestsList/UpdateContestModal.tsx new file mode 100644 index 00000000..01a0e43c --- /dev/null +++ b/src/pages/admin/AdminContestsList/UpdateContestModal.tsx @@ -0,0 +1,76 @@ +import { useCallback, useEffect, useState } from "react" +import { Button } from "../../../components/Button" +import { Column, Row } from "../../../components/Layout" +import LoadingContainer from "../../../components/LoadingContainer/LoadingContainer" +import Modal, { Props as ModalProps } from "../../../components/Modal/Modal" +import { Text } from "../../../components/Text" +import { Title } from "../../../components/Title" +import { useAdminCreateContest } from "../../../hooks/api/admin/useAdminCreateContest" +import { ErrorModal } from "../../../pages/ContestDetails/ErrorModal" +import { CreateContestForm } from "./CreateContestForm" +import { ContestsListItem } from "../../../hooks/api/admin/useAdminContests" + +type Props = ModalProps & { + contest: ContestsListItem +} + +export const UpdateContestModal: React.FC = ({ onClose, contest }) => { + const [formIsDirty, setFormIsDirty] = useState(false) + const { createContest, isLoading, isSuccess, error, reset } = useAdminCreateContest() + + const [displayModalCloseConfirm, setDisplayModalFormConfirm] = useState(false) + + useEffect(() => { + if (isSuccess) onClose?.() + }, [isSuccess, onClose]) + + const handleModalClose = useCallback(() => { + if (formIsDirty) { + setDisplayModalFormConfirm(true) + } else { + onClose && onClose() + } + }, [setDisplayModalFormConfirm, onClose, formIsDirty]) + + const handleModalCloseConfirm = useCallback(() => { + onClose && onClose() + }, [onClose]) + + const handleModalCloseCancel = useCallback(() => { + setDisplayModalFormConfirm(false) + }, []) + + return ( + + {displayModalCloseConfirm && ( + + + Unsaved contest + + Are you sure you want to close this form? All unsaved changes will be lost and you will need to start + over. + + + + + + + + )} + + + Edit {contest.title} + + + + {error && reset()} />} + + ) +} From 3c9ed3365d39a5eb0c2ea7c5e8b00a2bdac9163b Mon Sep 17 00:00:00 2001 From: Fran Rimoldi Date: Tue, 22 Aug 2023 20:55:01 -0300 Subject: [PATCH 21/80] fix: dirty form indicator --- .../AdminContestsList/CreateContestForm.tsx | 47 ++++++++++++++----- 1 file changed, 34 insertions(+), 13 deletions(-) diff --git a/src/pages/admin/AdminContestsList/CreateContestForm.tsx b/src/pages/admin/AdminContestsList/CreateContestForm.tsx index 580d797d..a00e4133 100644 --- a/src/pages/admin/AdminContestsList/CreateContestForm.tsx +++ b/src/pages/admin/AdminContestsList/CreateContestForm.tsx @@ -17,6 +17,7 @@ import TokenInput from "../../../components/TokenInput/TokenInput" import { Button } from "../../../components/Button" import { Contest } from "../../../hooks/api/contests" import { ContestsListItem } from "../../../hooks/api/admin/useAdminContests" +import { start } from "repl" type ContestValues = { protocol: { @@ -202,20 +203,40 @@ export const CreateContestForm: React.FC = ({ onSubmit, onDirtyChange, su ]) useEffect(() => { - onDirtyChange( - (protocolName !== "" || - contestTitle !== "" || - contestShortDescription !== "" || - contestNSLOC !== "" || - contestStartDate !== "" || - contestAuditLength !== "" || - contestAuditRewards?.gt(BigNumber.from(0)) || - contestJudgingPrizePool?.gt(BigNumber.from(0)) || - contestLeadJudgeFixedPay?.gt(BigNumber.from(0)) || - contestTotalCost?.gt(BigNumber.from(0))) ?? - false - ) + if (contest) { + const startDate = DateTime.fromSeconds(contest.startDate) + const endDate = DateTime.fromSeconds(contest.endDate) + const auditLength = endDate.diff(startDate, "days").days + + onDirtyChange( + (contestTitle !== contest.title || + contestShortDescription !== contest.shortDescription || + contestNSLOC !== contest.linesOfCode || + contestStartDate !== startDate.toFormat(DATE_FORMAT) || + contestAuditLength !== auditLength.toString() || + !contestAuditRewards?.eq(ethers.utils.parseUnits(`${contest.rewards}`, 6)) || + !contestJudgingPrizePool?.eq(ethers.utils.parseUnits(`${contest.judgingPrizePool}`, 6)) || + !contestLeadJudgeFixedPay?.eq(ethers.utils.parseUnits(`${contest.leadJudgeFixedPay}`, 6)) || + !contestTotalCost?.eq(ethers.utils.parseUnits(`${contest.fullPayment}`, 6))) ?? + false + ) + } else { + onDirtyChange( + (protocolName !== "" || + contestTitle !== "" || + contestShortDescription !== "" || + contestNSLOC !== "" || + contestStartDate !== "" || + contestAuditLength !== "" || + contestAuditRewards?.gt(BigNumber.from(0)) || + contestJudgingPrizePool?.gt(BigNumber.from(0)) || + contestLeadJudgeFixedPay?.gt(BigNumber.from(0)) || + contestTotalCost?.gt(BigNumber.from(0))) ?? + false + ) + } }, [ + contest, contestAuditLength, contestAuditRewards, contestJudgingPrizePool, From 7e96d94c034202096605a73fbff503a3664fb155 Mon Sep 17 00:00:00 2001 From: Fran Rimoldi Date: Wed, 23 Aug 2023 15:32:05 -0300 Subject: [PATCH 22/80] feat: create update contest hook --- src/hooks/api/admin/useAdminUpdateContest.ts | 54 +++++++++++++++++++ src/hooks/api/urls.ts | 1 + .../AdminContestsList/CreateContestForm.tsx | 6 +-- .../AdminContestsList/UpdateContestModal.tsx | 18 +++++-- 4 files changed, 70 insertions(+), 9 deletions(-) create mode 100644 src/hooks/api/admin/useAdminUpdateContest.ts diff --git a/src/hooks/api/admin/useAdminUpdateContest.ts b/src/hooks/api/admin/useAdminUpdateContest.ts new file mode 100644 index 00000000..33951d4f --- /dev/null +++ b/src/hooks/api/admin/useAdminUpdateContest.ts @@ -0,0 +1,54 @@ +import { DateTime } from "luxon" +import { useMutation, useQueryClient } from "react-query" +import { contests as contestsAPI } from "../axios" +import { adminUpdateContest as adminUpdateContestUrl } from "../urls" +import { AxiosError } from "axios" +import { adminContestsQuery } from "./useAdminContests" + +type AdminUpdateContestParams = { + id: number + title: string + shortDescription: string + nSLOC: string + startDate: DateTime + endDate: DateTime + auditRewards: number + judgingPrizePool: number + leadJudgeFixedPay: number + fullPayment: number +} + +export const useAdminUpdateContest = () => { + const queryClient = useQueryClient() + const { mutate, mutateAsync, ...mutation } = useMutation( + async (params) => { + try { + await contestsAPI.put(adminUpdateContestUrl(params.id), { + title: params.title, + short_description: params.shortDescription, + lines_of_code: params.nSLOC, + starts_at: params.startDate.toSeconds(), + ends_at: params.endDate.toSeconds(), + audit_rewards: params.auditRewards, + judging_prize_pool: params.judgingPrizePool, + lead_judge_fixed_pay: params.leadJudgeFixedPay, + full_payment: params.fullPayment, + }) + } catch (error) { + const axiosError = error as AxiosError + throw Error(axiosError.response?.data.error ?? "Something went wrong. Please, try again.") + } + }, + { + onSuccess: async () => { + await queryClient.invalidateQueries(adminContestsQuery("active")) + await queryClient.invalidateQueries(adminContestsQuery("finished")) + }, + } + ) + + return { + updateContest: mutate, + ...mutation, + } +} diff --git a/src/hooks/api/urls.ts b/src/hooks/api/urls.ts index 03afa6b9..fca130c3 100644 --- a/src/hooks/api/urls.ts +++ b/src/hooks/api/urls.ts @@ -67,6 +67,7 @@ export const adminApproveStart = () => `/admin/approve_start` export const getAdminProtocol = (name: string) => `/admin/protocol/${name}` export const getAdminContestScope = (contestID: number) => `/admin/contest/${contestID}/scope` export const adminCreateContest = () => `/admin/contests` +export const adminUpdateContest = (contestID: number) => `/admin/contests/${contestID}` export const getAdminTwitterAccount = (handle: string) => `/admin/twitter_account/${handle}` export const adminSubmitScope = () => `/admin/scope` export const getSeniorWatson = (handle: string) => `/admin/senior_watson?handle=${handle}` diff --git a/src/pages/admin/AdminContestsList/CreateContestForm.tsx b/src/pages/admin/AdminContestsList/CreateContestForm.tsx index a00e4133..d7885695 100644 --- a/src/pages/admin/AdminContestsList/CreateContestForm.tsx +++ b/src/pages/admin/AdminContestsList/CreateContestForm.tsx @@ -15,11 +15,9 @@ import { commify } from "../../../utils/units" import { Text } from "../../../components/Text" import TokenInput from "../../../components/TokenInput/TokenInput" import { Button } from "../../../components/Button" -import { Contest } from "../../../hooks/api/contests" import { ContestsListItem } from "../../../hooks/api/admin/useAdminContests" -import { start } from "repl" -type ContestValues = { +export type ContestValues = { protocol: { id?: number name: string @@ -173,14 +171,12 @@ export const CreateContestForm: React.FC = ({ onSubmit, onDirtyChange, su if (contestTitle === "") return false if (contestShortDescription.length < 100 || contestShortDescription.length > 200) return false - if (contestAuditLength === "") return false const startDate = DateTime.fromFormat(contestStartDate, DATE_FORMAT) if (!startDate.isValid) return false if (startDate < DateTime.now()) return false - if (contestAuditRewards?.eq(BigNumber.from(0))) return false if (contestTotalCost?.eq(BigNumber.from(0))) return false diff --git a/src/pages/admin/AdminContestsList/UpdateContestModal.tsx b/src/pages/admin/AdminContestsList/UpdateContestModal.tsx index 01a0e43c..8f1dd151 100644 --- a/src/pages/admin/AdminContestsList/UpdateContestModal.tsx +++ b/src/pages/admin/AdminContestsList/UpdateContestModal.tsx @@ -5,10 +5,10 @@ import LoadingContainer from "../../../components/LoadingContainer/LoadingContai import Modal, { Props as ModalProps } from "../../../components/Modal/Modal" import { Text } from "../../../components/Text" import { Title } from "../../../components/Title" -import { useAdminCreateContest } from "../../../hooks/api/admin/useAdminCreateContest" import { ErrorModal } from "../../../pages/ContestDetails/ErrorModal" -import { CreateContestForm } from "./CreateContestForm" +import { ContestValues, CreateContestForm } from "./CreateContestForm" import { ContestsListItem } from "../../../hooks/api/admin/useAdminContests" +import { useAdminUpdateContest } from "../../../hooks/api/admin/useAdminUpdateContest" type Props = ModalProps & { contest: ContestsListItem @@ -16,7 +16,7 @@ type Props = ModalProps & { export const UpdateContestModal: React.FC = ({ onClose, contest }) => { const [formIsDirty, setFormIsDirty] = useState(false) - const { createContest, isLoading, isSuccess, error, reset } = useAdminCreateContest() + const { updateContest, isLoading, isSuccess, error, reset } = useAdminUpdateContest() const [displayModalCloseConfirm, setDisplayModalFormConfirm] = useState(false) @@ -40,6 +40,16 @@ export const UpdateContestModal: React.FC = ({ onClose, contest }) => { setDisplayModalFormConfirm(false) }, []) + const handleFormSubmit = useCallback( + (values: ContestValues) => { + updateContest({ + id: contest.id, + ...values.contest, + }) + }, + [contest.id, updateContest] + ) + return ( {displayModalCloseConfirm && ( @@ -63,7 +73,7 @@ export const UpdateContestModal: React.FC = ({ onClose, contest }) => { Edit {contest.title} Date: Wed, 23 Aug 2023 16:21:58 -0300 Subject: [PATCH 23/80] feat: add edit icon --- .../AdminContestsListActive.tsx | 23 ++++--------------- 1 file changed, 5 insertions(+), 18 deletions(-) diff --git a/src/pages/admin/AdminContestsList/AdminContestsListActive.tsx b/src/pages/admin/AdminContestsList/AdminContestsListActive.tsx index 81271478..1aa73a3b 100644 --- a/src/pages/admin/AdminContestsList/AdminContestsListActive.tsx +++ b/src/pages/admin/AdminContestsList/AdminContestsListActive.tsx @@ -1,6 +1,6 @@ import { DateTime } from "luxon" import { useCallback, useState } from "react" -import { FaClipboardList, FaEye, FaFastForward, FaPlus, FaBullseye } from "react-icons/fa" +import { FaClipboardList, FaFastForward, FaPlus, FaEdit } from "react-icons/fa" import { Box } from "../../../components/Box" import { Button } from "../../../components/Button" import { Column, Row } from "../../../components/Layout" @@ -296,12 +296,7 @@ export const AdminContestsListActive = () => { {c.id} - {c.title} setUpdateContextIndex(index)} - /> + {c.title} {c.title} @@ -323,18 +318,10 @@ export const AdminContestsListActive = () => { - From 77eaf401e203a4bcfbbd85351f1bde3ecf93e7ab Mon Sep 17 00:00:00 2001 From: Fran Rimoldi Date: Fri, 25 Aug 2023 10:15:51 -0300 Subject: [PATCH 24/80] feat: add extra nsloc field --- src/hooks/api/admin/useAdminSubmitScope.ts | 2 ++ src/pages/admin/AdminScope/AdminScope.tsx | 12 +++++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/hooks/api/admin/useAdminSubmitScope.ts b/src/hooks/api/admin/useAdminSubmitScope.ts index 588c17d4..b3880121 100644 --- a/src/hooks/api/admin/useAdminSubmitScope.ts +++ b/src/hooks/api/admin/useAdminSubmitScope.ts @@ -9,6 +9,7 @@ type AdminSubmitScopeParams = { branchName: string commitHash: string files: string[] + nSLOCExtra?: number } type AdminSubmitScopeResponse = { @@ -38,6 +39,7 @@ export const useAdminSubmitScope = () => { branch_name: params.branchName, commit_hash: params.commitHash, files: params.files, + nsloc_extra: params.nSLOCExtra, }, { timeout: 5 * 60 * 1000, diff --git a/src/pages/admin/AdminScope/AdminScope.tsx b/src/pages/admin/AdminScope/AdminScope.tsx index ba457949..5c8b8c60 100644 --- a/src/pages/admin/AdminScope/AdminScope.tsx +++ b/src/pages/admin/AdminScope/AdminScope.tsx @@ -22,6 +22,7 @@ export const AdminScope = () => { const [debouncedRepoName] = useDebounce(repoName, 300) const [branchName, setBranchName] = useState() const [commitHash, setCommitHash] = useState() + const [nSLOCExtra, setNSLOCExtra] = useState() const [files, setFiles] = useState([]) const [branchSelectionModalOpen, setBranchSelectionModalOpen] = useState(false) @@ -99,8 +100,9 @@ export const AdminScope = () => { branchName: branchName!, commitHash: commitHash!, files, + nSLOCExtra, }) - }, [canGenerateReport, submitScope, repo, branchName, commitHash, files]) + }, [canGenerateReport, submitScope, repo, branchName, commitHash, files, nSLOCExtra]) const handleDownloadReport = useCallback(() => { if (report?.url) { @@ -140,6 +142,14 @@ export const AdminScope = () => { )} + {repo && ( + + + Extra nSLOC + + + + )} {repo && ( {report ? ( From cab0499640cc92157d8a7ff7cff293cbdbddcc01 Mon Sep 17 00:00:00 2001 From: Fran Rimoldi Date: Fri, 25 Aug 2023 19:06:55 -0300 Subject: [PATCH 25/80] feat: display nsloc in admin scope --- src/hooks/api/scope/useRepositoryContracts.ts | 9 ++++++--- src/pages/admin/AdminScope/AdminScope.tsx | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/hooks/api/scope/useRepositoryContracts.ts b/src/hooks/api/scope/useRepositoryContracts.ts index 5511cf24..c7bd7159 100644 --- a/src/hooks/api/scope/useRepositoryContracts.ts +++ b/src/hooks/api/scope/useRepositoryContracts.ts @@ -2,7 +2,10 @@ import { useQuery } from "react-query" import { contests as contestsAPI } from "../axios" import { getRepositoryContracts as getRepositoryContractsUrl } from "../urls" -type GetRepositoryContractsResponse = string[] +type GetRepositoryContractsResponse = { + file_path: string + nsloc?: number +}[] type File = { filepath: string @@ -101,10 +104,10 @@ export const useRepositoryContracts = (repo: string, commit: string) => useQuery(repositoryContractsQuery(repo, commit), async () => { const { data } = await contestsAPI.get(getRepositoryContractsUrl(repo, commit)) - const tree = convertToTree2(data.map((f) => ({ filepath: f }))) + const tree = convertToTree2(data.map((f) => ({ filepath: f.file_path, nsloc: f.nsloc }))) return { tree, - rawPaths: data, + rawPaths: data.map((f) => f.file_path), } }) diff --git a/src/pages/admin/AdminScope/AdminScope.tsx b/src/pages/admin/AdminScope/AdminScope.tsx index 5c8b8c60..a7c17032 100644 --- a/src/pages/admin/AdminScope/AdminScope.tsx +++ b/src/pages/admin/AdminScope/AdminScope.tsx @@ -145,7 +145,7 @@ export const AdminScope = () => { {repo && ( - Extra nSLOC + nSLOC Adjustment From df67238f3aa43ca398c6ac64dd03698f6f251971 Mon Sep 17 00:00:00 2001 From: Fran Rimoldi Date: Fri, 25 Aug 2023 19:46:50 -0300 Subject: [PATCH 26/80] feat: admin scope download report --- src/hooks/api/admin/useAdminSubmitScope.ts | 19 +++------- src/pages/admin/AdminScope/AdminScope.tsx | 36 ++++++------------- .../AdminScope/SaveScopeSuccessModal.tsx | 28 +++++++++++++++ 3 files changed, 44 insertions(+), 39 deletions(-) create mode 100644 src/pages/admin/AdminScope/SaveScopeSuccessModal.tsx diff --git a/src/hooks/api/admin/useAdminSubmitScope.ts b/src/hooks/api/admin/useAdminSubmitScope.ts index b3880121..e7d7aa5b 100644 --- a/src/hooks/api/admin/useAdminSubmitScope.ts +++ b/src/hooks/api/admin/useAdminSubmitScope.ts @@ -9,20 +9,14 @@ type AdminSubmitScopeParams = { branchName: string commitHash: string files: string[] - nSLOCExtra?: number + nSLOCAdjustment?: number } type AdminSubmitScopeResponse = { - report: { - url: string - nSLOC: number - } + report: string } -type Report = { - url: string - nSLOC: number -} +type Report = string export const useAdminSubmitScope = () => { const { @@ -39,17 +33,14 @@ export const useAdminSubmitScope = () => { branch_name: params.branchName, commit_hash: params.commitHash, files: params.files, - nsloc_extra: params.nSLOCExtra, + nsloc_adjustment: params.nSLOCAdjustment, }, { timeout: 5 * 60 * 1000, } ) - return { - url: data.report.url, - nSLOC: data.report.nSLOC, - } + return data.report } catch (error) { const axiosError = error as AxiosError throw Error(axiosError.response?.data.error ?? "Something went wrong. Please, try again.") diff --git a/src/pages/admin/AdminScope/AdminScope.tsx b/src/pages/admin/AdminScope/AdminScope.tsx index a7c17032..7940033d 100644 --- a/src/pages/admin/AdminScope/AdminScope.tsx +++ b/src/pages/admin/AdminScope/AdminScope.tsx @@ -1,5 +1,5 @@ import { useCallback, useEffect, useMemo, useState } from "react" -import { FaCopy, FaDownload, FaGithub } from "react-icons/fa" +import { FaGithub } from "react-icons/fa" import { useDebounce } from "use-debounce" import { Box } from "../../../components/Box" import { Button } from "../../../components/Button" @@ -15,6 +15,7 @@ import { BranchSelectionModal } from "../../AuditScope/BranchSelectionModal" import { CommitSelectionModal } from "../../AuditScope/CommitSelectionModal" import { RepositoryContractsSelector } from "../../AuditScope/RepositoryContractsSelector" import { useRepositoryContracts } from "../../../hooks/api/scope/useRepositoryContracts" +import { SaveScopeSuccessModal } from "./SaveScopeSuccessModal" export const AdminScope = () => { const [repoLink, setRepoLink] = useState("") @@ -22,14 +23,14 @@ export const AdminScope = () => { const [debouncedRepoName] = useDebounce(repoName, 300) const [branchName, setBranchName] = useState() const [commitHash, setCommitHash] = useState() - const [nSLOCExtra, setNSLOCExtra] = useState() + const [nSLOCAdjustment, setNSLOCAdjustment] = useState() const [files, setFiles] = useState([]) const [branchSelectionModalOpen, setBranchSelectionModalOpen] = useState(false) const [commitSelectionModalOpen, setCommitSelectionModalOpen] = useState(false) const { data: repo, isLoading: repoIsLoading } = useRepository(debouncedRepoName) - const { submitScope, isLoading, data: report } = useAdminSubmitScope() + const { submitScope, isLoading, data: report, isSuccess } = useAdminSubmitScope() const { data: repoContracts } = useRepositoryContracts(debouncedRepoName, commitHash ?? "") @@ -100,19 +101,9 @@ export const AdminScope = () => { branchName: branchName!, commitHash: commitHash!, files, - nSLOCExtra, + nSLOCAdjustment, }) - }, [canGenerateReport, submitScope, repo, branchName, commitHash, files, nSLOCExtra]) - - const handleDownloadReport = useCallback(() => { - if (report?.url) { - window.open(report.url, "blank") - } - }, [report]) - - const handleCopyLink = useCallback(async () => { - await navigator.clipboard.writeText(report?.url ?? "") - }, [report?.url]) + }, [canGenerateReport, submitScope, repo, branchName, commitHash, files, nSLOCAdjustment]) return ( @@ -146,21 +137,15 @@ export const AdminScope = () => { nSLOC Adjustment - + )} {repo && ( - {report ? ( - - - - ) : ( - - )} + )} @@ -185,6 +170,7 @@ export const AdminScope = () => { )} + {isSuccess && } {branchSelectionModalOpen && ( = ({ reportURL, ...props }) => { + const handleDownloadClick = useCallback(() => { + window.open(reportURL, "blank") + }, [reportURL]) + + return ( + + + Scope saved + + + + ) +} From a0cb49ccb29d967541bbb4ee9e4d7261a344de75 Mon Sep 17 00:00:00 2001 From: Fran Rimoldi Date: Tue, 29 Aug 2023 11:35:24 -0300 Subject: [PATCH 27/80] fix: remove opyn as default selection --- src/components/Select/Select.tsx | 6 +++--- src/pages/Claim/Claims.tsx | 2 +- src/pages/Protocol/Protocol.tsx | 23 ++++++++++++++++------- 3 files changed, 20 insertions(+), 11 deletions(-) diff --git a/src/components/Select/Select.tsx b/src/components/Select/Select.tsx index 0136b17b..baa71201 100644 --- a/src/components/Select/Select.tsx +++ b/src/components/Select/Select.tsx @@ -6,13 +6,13 @@ import { FaCaretDown } from "react-icons/fa" import Option from "./Option" type OptionType = { - value: T + value?: T label: string } type Props = { options: Array> - onChange: (value: T) => void + onChange: (value?: T) => void value?: T placeholder?: string } @@ -36,7 +36,7 @@ const Select = ({ options, onChange, value, placeholder }: Props) => { const [optionsVisible, setOptionsVisible] = React.useState(false) const handleUpdateSelectedOption = React.useCallback( - (option: T) => { + (option: T | undefined) => { setOptionsVisible(false) onChange?.(option) }, diff --git a/src/pages/Claim/Claims.tsx b/src/pages/Claim/Claims.tsx index 901ea73a..205fa34e 100644 --- a/src/pages/Claim/Claims.tsx +++ b/src/pages/Claim/Claims.tsx @@ -44,7 +44,7 @@ export const ClaimsPage: React.FC = () => { /** * Handler for protocol's select option change */ - const handleProtocolChanged = useCallback((option: string) => { + const handleProtocolChanged = useCallback((option?: string) => { setSelectedProtocolBytesIdentifier(option) }, []) diff --git a/src/pages/Protocol/Protocol.tsx b/src/pages/Protocol/Protocol.tsx index 8f495cf7..b6b63661 100644 --- a/src/pages/Protocol/Protocol.tsx +++ b/src/pages/Protocol/Protocol.tsx @@ -28,16 +28,25 @@ export const ProtocolPage: React.FC = () => { const { data: protocols } = useProtocols() const { address: connectedAddress } = useAccount() - const protocolSelectOptions = React.useMemo( - () => - Object.entries(protocols ?? {}) + const protocolSelectOptions = React.useMemo(() => { + const options = [ + ...(!selectedProtocolId + ? [ + { + value: undefined, + label: "Select Protocol", + }, + ] + : []), + ...(Object.entries(protocols ?? {}) .filter(([_, p]) => p.agent !== ethers.constants.AddressZero) .map(([key, item]) => ({ label: item.name ?? "Unknown", value: key as `0x${string}`, - })) ?? [], - [protocols] - ) + })) ?? []), + ] + return options + }, [protocols, selectedProtocolId]) const selectedProtocol = React.useMemo( () => (selectedProtocolId ? protocols?.[selectedProtocolId] ?? null : null), [selectedProtocolId, protocols] @@ -73,7 +82,7 @@ export const ProtocolPage: React.FC = () => { /** * Handler for changing the protocol */ - const handleOnProtocolChanged = React.useCallback((option: `0x${string}`) => { + const handleOnProtocolChanged = React.useCallback((option?: `0x${string}`) => { setSelectedProtocolId(option) }, []) From b163d127ae24b1e1b69bb05f1afdc7c62e7a30d9 Mon Sep 17 00:00:00 2001 From: Fran Rimoldi Date: Thu, 31 Aug 2023 18:55:16 -0300 Subject: [PATCH 28/80] feat: admin reset scope button --- src/hooks/api/admin/useAdminResetScope.ts | 18 ++++++++++++++++++ src/hooks/api/urls.ts | 1 + .../AdminContestsListActive.tsx | 10 +++++++++- .../AdminContestsList/ContestScopeModal.tsx | 9 +++++++-- 4 files changed, 35 insertions(+), 3 deletions(-) create mode 100644 src/hooks/api/admin/useAdminResetScope.ts diff --git a/src/hooks/api/admin/useAdminResetScope.ts b/src/hooks/api/admin/useAdminResetScope.ts new file mode 100644 index 00000000..fa2fa918 --- /dev/null +++ b/src/hooks/api/admin/useAdminResetScope.ts @@ -0,0 +1,18 @@ +import { useMutation } from "wagmi" +import { adminResetScope as adminResetScopeUrl } from "../urls" +import { contests as contestsAPI } from "../axios" + +type AdminResetScopeParams = { + contestID: number +} + +export const useAdminResetScope = () => { + const { mutate, mutateAsync, ...mutation } = useMutation(async (params) => { + await contestsAPI.delete(adminResetScopeUrl(params.contestID)) + }) + + return { + resetScope: mutate, + ...mutation, + } +} diff --git a/src/hooks/api/urls.ts b/src/hooks/api/urls.ts index fca130c3..531db8af 100644 --- a/src/hooks/api/urls.ts +++ b/src/hooks/api/urls.ts @@ -68,6 +68,7 @@ export const getAdminProtocol = (name: string) => `/admin/protocol/${name}` export const getAdminContestScope = (contestID: number) => `/admin/contest/${contestID}/scope` export const adminCreateContest = () => `/admin/contests` export const adminUpdateContest = (contestID: number) => `/admin/contests/${contestID}` +export const adminResetScope = (contestID: number) => `/admin/contests/${contestID}/scope` export const getAdminTwitterAccount = (handle: string) => `/admin/twitter_account/${handle}` export const adminSubmitScope = () => `/admin/scope` export const getSeniorWatson = (handle: string) => `/admin/senior_watson?handle=${handle}` diff --git a/src/pages/admin/AdminContestsList/AdminContestsListActive.tsx b/src/pages/admin/AdminContestsList/AdminContestsListActive.tsx index 1aa73a3b..c5f7427a 100644 --- a/src/pages/admin/AdminContestsList/AdminContestsListActive.tsx +++ b/src/pages/admin/AdminContestsList/AdminContestsListActive.tsx @@ -1,6 +1,6 @@ import { DateTime } from "luxon" import { useCallback, useState } from "react" -import { FaClipboardList, FaFastForward, FaPlus, FaEdit } from "react-icons/fa" +import { FaClipboardList, FaFastForward, FaPlus, FaEdit, FaRegListAlt } from "react-icons/fa" import { Box } from "../../../components/Box" import { Button } from "../../../components/Button" import { Column, Row } from "../../../components/Layout" @@ -323,6 +323,14 @@ export const AdminContestsListActive = () => { > + diff --git a/src/pages/admin/AdminContestsList/ContestScopeModal.tsx b/src/pages/admin/AdminContestsList/ContestScopeModal.tsx index 5908a2f2..70964679 100644 --- a/src/pages/admin/AdminContestsList/ContestScopeModal.tsx +++ b/src/pages/admin/AdminContestsList/ContestScopeModal.tsx @@ -11,6 +11,7 @@ import { useAdminContestScope } from "../../../hooks/api/admin/useAdminContestSc import { useContest } from "../../../hooks/api/contests" import styles from "./AdminContestsList.module.scss" +import { useAdminResetScope } from "../../../hooks/api/admin/useAdminResetScope" type Props = ModalProps & { contestID: number @@ -21,13 +22,14 @@ const COMMENT_TO_SOURCE_MIN = 0.8 export const ContestScopeModal: React.FC = ({ onClose, contestID }) => { const { data: contest, isLoading: contestIsLoading } = useContest(contestID) const { data: scope, isLoading: scopeIsLoading } = useAdminContestScope(contestID) + const { resetScope, isLoading: resetScopeIsLoading } = useAdminResetScope() const submittedNSLOC = scope?.reduce((t, s) => t + (s.files.reduce((t, f) => t + (f.nSLOC ?? 0), 0) ?? 0), 0) ?? 0 const expectedNSLOCExceeded = contest && scope && (parseInt(contest.linesOfCode ?? "") ?? 0) < (submittedNSLOC ?? 0) return ( - + {contest?.title} {expectedNSLOCExceeded ? ( @@ -89,7 +91,7 @@ export const ContestScopeModal: React.FC = ({ onClose, contestID }) => { From 398002b8d81ea772d2d2f59c8fa2fd1afe324dc8 Mon Sep 17 00:00:00 2001 From: Fran Rimoldi Date: Fri, 1 Sep 2023 17:34:41 -0300 Subject: [PATCH 29/80] fix: send undefined values to api --- .../AdminContestsListActive.tsx | 10 +++++++++- .../AdminContestsList/CreateContestForm.tsx | 20 +++++++++---------- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/src/pages/admin/AdminContestsList/AdminContestsListActive.tsx b/src/pages/admin/AdminContestsList/AdminContestsListActive.tsx index 1aa73a3b..3255fe11 100644 --- a/src/pages/admin/AdminContestsList/AdminContestsListActive.tsx +++ b/src/pages/admin/AdminContestsList/AdminContestsListActive.tsx @@ -1,6 +1,6 @@ import { DateTime } from "luxon" import { useCallback, useState } from "react" -import { FaClipboardList, FaFastForward, FaPlus, FaEdit } from "react-icons/fa" +import { FaClipboardList, FaFastForward, FaPlus, FaEdit, FaCode } from "react-icons/fa" import { Box } from "../../../components/Box" import { Button } from "../../../components/Button" import { Column, Row } from "../../../components/Layout" @@ -323,6 +323,14 @@ export const AdminContestsListActive = () => { > + diff --git a/src/pages/admin/AdminContestsList/CreateContestForm.tsx b/src/pages/admin/AdminContestsList/CreateContestForm.tsx index d7885695..f0575008 100644 --- a/src/pages/admin/AdminContestsList/CreateContestForm.tsx +++ b/src/pages/admin/AdminContestsList/CreateContestForm.tsx @@ -20,11 +20,11 @@ import { ContestsListItem } from "../../../hooks/api/admin/useAdminContests" export type ContestValues = { protocol: { id?: number - name: string - twitter: string - githubTeam: string - website: string - logoUrl: string + name?: string + twitter?: string + githubTeam?: string + website?: string + logoUrl?: string } contest: { title: string @@ -273,11 +273,11 @@ export const CreateContestForm: React.FC = ({ onSubmit, onDirtyChange, su onSubmit({ protocol: { id: protocol?.id, - name: protocolName, - githubTeam: protocolGithubTeam, - twitter: protocolTwitter, - website: protocolWebsite, - logoUrl: protocolLogoURL, + name: protocolName === "" ? undefined : protocolName, + githubTeam: protocolGithubTeam === "" ? undefined : protocolGithubTeam, + twitter: protocolTwitter === "" ? undefined : protocolTwitter, + website: protocolWebsite === "" ? undefined : protocolWebsite, + logoUrl: protocolLogoURL === "" ? undefined : protocolLogoURL, }, contest: { title: contestTitle, From 0afa830bdd08bad35e6e60639661fe0ef82c97ed Mon Sep 17 00:00:00 2001 From: Fran Rimoldi Date: Fri, 1 Sep 2023 17:36:47 -0300 Subject: [PATCH 30/80] fix --- .../AdminContestsList/AdminContestsListActive.tsx | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/pages/admin/AdminContestsList/AdminContestsListActive.tsx b/src/pages/admin/AdminContestsList/AdminContestsListActive.tsx index 3255fe11..1aa73a3b 100644 --- a/src/pages/admin/AdminContestsList/AdminContestsListActive.tsx +++ b/src/pages/admin/AdminContestsList/AdminContestsListActive.tsx @@ -1,6 +1,6 @@ import { DateTime } from "luxon" import { useCallback, useState } from "react" -import { FaClipboardList, FaFastForward, FaPlus, FaEdit, FaCode } from "react-icons/fa" +import { FaClipboardList, FaFastForward, FaPlus, FaEdit } from "react-icons/fa" import { Box } from "../../../components/Box" import { Button } from "../../../components/Button" import { Column, Row } from "../../../components/Layout" @@ -323,14 +323,6 @@ export const AdminContestsListActive = () => { > - From c9ed1e8e36b1161a39e2c1484ee448cac693cfe7 Mon Sep 17 00:00:00 2001 From: Fran Rimoldi Date: Tue, 26 Sep 2023 15:04:42 -0300 Subject: [PATCH 31/80] feat: add Ajna's claim --- src/components/ClaimsList/ClaimsList.tsx | 52 ++++++++++++++++++++---- 1 file changed, 44 insertions(+), 8 deletions(-) diff --git a/src/components/ClaimsList/ClaimsList.tsx b/src/components/ClaimsList/ClaimsList.tsx index 9d3312ae..7e89f387 100644 --- a/src/components/ClaimsList/ClaimsList.tsx +++ b/src/components/ClaimsList/ClaimsList.tsx @@ -7,7 +7,18 @@ import { formatAmount } from "../../utils/format" import styles from "./ClaimsList.module.scss" import cx from "classnames" -const CLAIMS = [ +type Claim = { + id: number + protocol: string + date: string + coverageAgreementUrl: string + evidenceUrl?: string + amount: number + txHashUrl: string + status: "paid" | "denied" +} + +const CLAIMS: Claim[] = [ { id: 1, protocol: "Euler", @@ -18,6 +29,7 @@ const CLAIMS = [ "https://sherlock-files.ams3.digitaloceanspaces.com/claims/Euler_0x3019e52a670390f24e4b9b58af62a7367658e457bbb07f86b19b213ec74b5be7_16817996.pdf?hash=d6c8e864a5312384143a21f542e998f6e83357f5b8e3e34ccdcb551404d25c09", amount: 4_529_285, txHashUrl: "https://etherscan.io/tx/0x234cd8e369fdcd0387ada4214e563a6a72aad4abd0b464a2873f3eb9dac2579b", + status: "paid", }, { id: 2, @@ -29,9 +41,29 @@ const CLAIMS = [ "https://sherlock-files.ams3.digitaloceanspaces.com/claims/Sentiment_0x5af5b22283e35ef9d9d4a32753014cdc40fd7a5a5d920d83d2c1e901c10a0a7c_16977102.pdf?hash=f513422f0d98aaac6f669142caf9148172ca7f2a17559f3c1cb26177df11e8ef", amount: 65_701, txHashUrl: "https://etherscan.io/tx/0xe67bbb7a9085f1a603fcea1eefe08c7674a2f504e81f6d69df41c8b077c2765e", + status: "paid", + }, + { + id: 3, + protocol: "Ajna", + date: "Sept 26, 2023", + coverageAgreementUrl: + "https://github.com/sherlock-protocol/sherlock-reports/blob/main/coverage-agreements/Ajna%20Coverage%20Agreement%202023.07.15.pdf", + amount: 49_500, + txHashUrl: "https://etherscan.io/tx/0xa4a8fbea2c24662ea46e25ab3ab69456a8327463803c4851948c2ba2234fed5f", + status: "paid", }, ] +function getStatusLabel(status: "paid" | "denied") { + switch (status) { + case "paid": + return "Paid" + case "denied": + return "Denied" + } +} + /** * List of past claims */ @@ -64,7 +96,7 @@ const ClaimsList: React.FC = () => { Evidence - Transaction + Status @@ -89,16 +121,20 @@ const ClaimsList: React.FC = () => { - - - Link - - + {item.evidenceUrl ? ( + + + Link + + + ) : ( + - + )} - Link + {getStatusLabel(item.status)} From 53f0e873e3cabdc93cbc4037288736bac623f767 Mon Sep 17 00:00:00 2001 From: Fran Rimoldi Date: Tue, 26 Sep 2023 15:07:00 -0300 Subject: [PATCH 32/80] fix: typos --- src/pages/Claim/ClaimStatusAction.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/Claim/ClaimStatusAction.tsx b/src/pages/Claim/ClaimStatusAction.tsx index f5757285..5a8a4d80 100644 --- a/src/pages/Claim/ClaimStatusAction.tsx +++ b/src/pages/Claim/ClaimStatusAction.tsx @@ -187,10 +187,10 @@ const Escalate: React.FC = ({ claim, protocol }) => { {!connectedAccountIsClaimInitiator && ( <> - Only the claim inintiator can escalate it to UMA. + Only the claim initiator can escalate it to UMA. - Please connnect using account with address {shortenAddress(claim.initiator)} + Please connect using account with address {shortenAddress(claim.initiator)} )} From 10401a28d2117411654c56c02dbb58254f85766a Mon Sep 17 00:00:00 2001 From: Rares Stanciu Date: Tue, 10 Oct 2023 13:33:20 +0300 Subject: [PATCH 33/80] fix: render context question description html --- .../ContextQuestions/ContextQuestions.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/pages/protocol_dashboard/ContextQuestions/ContextQuestions.tsx b/src/pages/protocol_dashboard/ContextQuestions/ContextQuestions.tsx index cd14dadc..4232f568 100644 --- a/src/pages/protocol_dashboard/ContextQuestions/ContextQuestions.tsx +++ b/src/pages/protocol_dashboard/ContextQuestions/ContextQuestions.tsx @@ -13,6 +13,7 @@ import { useContextQuestions } from "../../../hooks/api/protocols/useContextQues import { useSubmitContextQuestionsAnswers } from "../../../hooks/api/protocols/useSubmitContextQuestionsAnswers" import { useUpdateContextQuestionAnswers } from "../../../hooks/api/protocols/useUpdateContextQuestionAnswers" import Modal, { Props as ModalProps } from "../../../components/Modal/Modal" +import * as DOMPurify from "dompurify" import styles from "./ContextQuestions.module.scss" import { ErrorModal } from "../../ContestDetails/ErrorModal" @@ -139,11 +140,16 @@ export const ContextQuestions = () => { {contextQuestions && contextQuestions.length > 0 ? ( contextQuestions?.map((q) => { const answer = answers.find((a) => a.questionID === q.id) + const cleanDescriptionHTML = DOMPurify.sanitize(q.description, { + USE_PROFILES: { html: true }, + ADD_ATTR: ["target"], + }) + return ( {q.question} - {q.description} + Date: Wed, 18 Oct 2023 11:08:51 -0300 Subject: [PATCH 34/80] feat: add total rewards extra field --- .../AdminContestsList/CreateContestForm.tsx | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/pages/admin/AdminContestsList/CreateContestForm.tsx b/src/pages/admin/AdminContestsList/CreateContestForm.tsx index f0575008..3e89f779 100644 --- a/src/pages/admin/AdminContestsList/CreateContestForm.tsx +++ b/src/pages/admin/AdminContestsList/CreateContestForm.tsx @@ -72,10 +72,13 @@ export const CreateContestForm: React.FC = ({ onSubmit, onDirtyChange, su const [debouncedContestNSLOC] = useDebounce(contestNSLOC, 300) const [contestStartDate, setContestStartDate] = useState("") const [contestAuditLength, setContestAuditLength] = useState("") + const [contestTotalRewards, setContestTotalRewards] = useState(BigNumber.from(0)) const [contestAuditRewards, setContestAuditRewards] = useState(BigNumber.from(0)) const [contestJudgingPrizePool, setContestJudgingPrizePool] = useState(BigNumber.from(0)) const [contestLeadJudgeFixedPay, setContestLeadJudgeFixedPay] = useState(BigNumber.from(0)) const [contestTotalCost, setContestTotalCost] = useState(BigNumber.from(0)) + + const [initialTotalRewards, setInitialTotalRewards] = useState(BigNumber.from(0)) const [initialAuditContestRewards, setInitialAuditContestRewards] = useState(BigNumber.from(0)) const [initialJudgingPrizePool, setInitialJudgingPrizePool] = useState(BigNumber.from(0)) const [initialLeadJudgeFixedPay, setInitialLeadJudgeFixedPay] = useState(BigNumber.from(0)) @@ -100,6 +103,9 @@ export const CreateContestForm: React.FC = ({ onSubmit, onDirtyChange, su setShortDescription(contest.shortDescription) setContestStartDate(startDate.toFormat(DATE_FORMAT)) setContestAuditLength(auditLength.toString()) + setInitialTotalRewards( + ethers.utils.parseUnits(`${contest.rewards + contest.judgingPrizePool + contest.leadJudgeFixedPay}`, 6) + ) setInitialAuditContestRewards(ethers.utils.parseUnits(`${contest.rewards}`, 6)) setInitialJudgingPrizePool(ethers.utils.parseUnits(`${contest.judgingPrizePool}`, 6)) setInitialLeadJudgeFixedPay(ethers.utils.parseUnits(`${contest.leadJudgeFixedPay}`, 6)) @@ -110,6 +116,16 @@ export const CreateContestForm: React.FC = ({ onSubmit, onDirtyChange, su useEffect(() => { if (contestVariablesSuccess && !contest) { setContestAuditLength(`${contestVariables.length}`) + setInitialTotalRewards( + ethers.utils.parseUnits( + `${ + contestVariables.auditContestRewards + + contestVariables.judgingPrizePool + + contestVariables.leadJudgeFixedPay + }`, + 6 + ) + ) setInitialAuditContestRewards(ethers.utils.parseUnits(`${contestVariables.auditContestRewards}`, 6)) setInitialJudgingPrizePool(ethers.utils.parseUnits(`${contestVariables.judgingPrizePool}`, 6)) setInitialLeadJudgeFixedPay(ethers.utils.parseUnits(`${contestVariables.leadJudgeFixedPay}`, 6)) @@ -117,6 +133,16 @@ export const CreateContestForm: React.FC = ({ onSubmit, onDirtyChange, su } }, [contestVariablesSuccess, setContestAuditLength, contestVariables, contest]) + useEffect(() => { + const diff = contestTotalRewards + ?.sub(contestAuditRewards ?? BigNumber.from(0)) + .sub(contestJudgingPrizePool ?? BigNumber.from(0)) + .sub(contestLeadJudgeFixedPay ?? BigNumber.from(0)) + + setInitialAuditContestRewards(contestAuditRewards?.add(diff ?? BigNumber.from(0))) + setInitialTotalCost(contestTotalCost?.add(diff ?? BigNumber.from(0))) + }, [contestTotalRewards, setInitialAuditContestRewards]) + useEffect(() => { if (protocol?.name) { setProtocolName(protocol?.name) @@ -410,6 +436,9 @@ export const CreateContestForm: React.FC = ({ onSubmit, onDirtyChange, su + + + From 9550838f702b5c26382c0cab9936d475e920693a Mon Sep 17 00:00:00 2001 From: Fran Rimoldi Date: Wed, 18 Oct 2023 17:49:28 -0300 Subject: [PATCH 35/80] fix: remove admin fees --- src/hooks/api/admin/useAdminContestVariables.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/hooks/api/admin/useAdminContestVariables.ts b/src/hooks/api/admin/useAdminContestVariables.ts index 4525899d..720bca50 100644 --- a/src/hooks/api/admin/useAdminContestVariables.ts +++ b/src/hooks/api/admin/useAdminContestVariables.ts @@ -7,7 +7,6 @@ type GetAdminContestVariablesResponse = { contest_rewards: number judging_prize_pool: number lead_judge_fixed_pay: number - admin_fee: number } type ContestVariables = { @@ -16,7 +15,6 @@ type ContestVariables = { auditContestRewards: number judgingPrizePool: number leadJudgeFixedPay: number - adminFee: number } export const adminContestVariablesQueryKey = (nSLOC: number) => ["contest-variables", nSLOC] @@ -30,6 +28,5 @@ export const useAdminContestVariables = (nSLOC: number) => auditContestRewards: data.contest_rewards, judgingPrizePool: data.judging_prize_pool, leadJudgeFixedPay: data.lead_judge_fixed_pay, - adminFee: data.admin_fee, } }) From de2e81b4e1b5c20f8c64cb6457c5f8862816bb0d Mon Sep 17 00:00:00 2001 From: Fran Rimoldi Date: Tue, 7 Nov 2023 15:26:03 +0100 Subject: [PATCH 36/80] fix: contest creation full amount --- src/pages/admin/AdminContestsList/CreateContestForm.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/admin/AdminContestsList/CreateContestForm.tsx b/src/pages/admin/AdminContestsList/CreateContestForm.tsx index 3e89f779..5cb4d476 100644 --- a/src/pages/admin/AdminContestsList/CreateContestForm.tsx +++ b/src/pages/admin/AdminContestsList/CreateContestForm.tsx @@ -140,7 +140,6 @@ export const CreateContestForm: React.FC = ({ onSubmit, onDirtyChange, su .sub(contestLeadJudgeFixedPay ?? BigNumber.from(0)) setInitialAuditContestRewards(contestAuditRewards?.add(diff ?? BigNumber.from(0))) - setInitialTotalCost(contestTotalCost?.add(diff ?? BigNumber.from(0))) }, [contestTotalRewards, setInitialAuditContestRewards]) useEffect(() => { From 6877a607fe88c87a1ddb8d30e8b214cfc66332d6 Mon Sep 17 00:00:00 2001 From: Fran Rimoldi Date: Wed, 8 Nov 2023 12:04:26 +0100 Subject: [PATCH 37/80] fix: timeout and error --- src/hooks/api/scope/useRepositoryContracts.ts | 27 ++++++++++++------- src/pages/admin/AdminScope/AdminScope.tsx | 22 ++++++++++++--- .../admin/AdminScope/ScopeErrorModal.tsx | 21 +++++++++++++++ 3 files changed, 58 insertions(+), 12 deletions(-) create mode 100644 src/pages/admin/AdminScope/ScopeErrorModal.tsx diff --git a/src/hooks/api/scope/useRepositoryContracts.ts b/src/hooks/api/scope/useRepositoryContracts.ts index c7bd7159..78edd0c1 100644 --- a/src/hooks/api/scope/useRepositoryContracts.ts +++ b/src/hooks/api/scope/useRepositoryContracts.ts @@ -101,13 +101,22 @@ type RepositoryContracts = { export const repositoryContractsQuery = (repo: string, commit: string) => ["repository-contracts", repo, commit] export const useRepositoryContracts = (repo: string, commit: string) => - useQuery(repositoryContractsQuery(repo, commit), async () => { - const { data } = await contestsAPI.get(getRepositoryContractsUrl(repo, commit)) - - const tree = convertToTree2(data.map((f) => ({ filepath: f.file_path, nsloc: f.nsloc }))) - - return { - tree, - rawPaths: data.map((f) => f.file_path), + useQuery( + repositoryContractsQuery(repo, commit), + async () => { + const { data } = await contestsAPI.get(getRepositoryContractsUrl(repo, commit), { + timeout: 5 * 60 * 1000, + }) + + const tree = convertToTree2(data.map((f) => ({ filepath: f.file_path, nsloc: f.nsloc }))) + + return { + tree, + rawPaths: data.map((f) => f.file_path), + } + }, + { + enabled: !!repo && !!commit, + refetchOnWindowFocus: false, } - }) + ) diff --git a/src/pages/admin/AdminScope/AdminScope.tsx b/src/pages/admin/AdminScope/AdminScope.tsx index 7940033d..1659493a 100644 --- a/src/pages/admin/AdminScope/AdminScope.tsx +++ b/src/pages/admin/AdminScope/AdminScope.tsx @@ -14,8 +14,10 @@ import { shortenCommitHash } from "../../../utils/repository" import { BranchSelectionModal } from "../../AuditScope/BranchSelectionModal" import { CommitSelectionModal } from "../../AuditScope/CommitSelectionModal" import { RepositoryContractsSelector } from "../../AuditScope/RepositoryContractsSelector" -import { useRepositoryContracts } from "../../../hooks/api/scope/useRepositoryContracts" +import { repositoryContractsQuery, useRepositoryContracts } from "../../../hooks/api/scope/useRepositoryContracts" import { SaveScopeSuccessModal } from "./SaveScopeSuccessModal" +import { ScopeErrorModal } from "./ScopeErrorModal" +import { useQueryClient } from "react-query" export const AdminScope = () => { const [repoLink, setRepoLink] = useState("") @@ -32,7 +34,13 @@ export const AdminScope = () => { const { data: repo, isLoading: repoIsLoading } = useRepository(debouncedRepoName) const { submitScope, isLoading, data: report, isSuccess } = useAdminSubmitScope() - const { data: repoContracts } = useRepositoryContracts(debouncedRepoName, commitHash ?? "") + const { + data: repoContracts, + isLoading: isLoadingContracts, + error: contractsError, + } = useRepositoryContracts(debouncedRepoName, commitHash ?? "") + + const queryClient = useQueryClient() useEffect(() => { const pattern = /^https?:\/\/github\.com\/([A-Za-z0-9-]+\/[A-Za-z0-9-]+)(?:\.git)?(?:\/tree\/([A-Za-z0-9-]+))?$/ @@ -51,6 +59,10 @@ export const AdminScope = () => { } }, [repo, branchName]) + const handleErrorModalClose = useCallback(() => { + window.location.reload() + }, []) + const handlePathSelected = useCallback( (selectedPaths: string[]) => { setFiles((f) => { @@ -106,7 +118,10 @@ export const AdminScope = () => { }, [canGenerateReport, submitScope, repo, branchName, commitHash, files, nSLOCAdjustment]) return ( - + @@ -171,6 +186,7 @@ export const AdminScope = () => { )} {isSuccess && } + {contractsError && } {branchSelectionModalOpen && ( = (props) => { + return ( + + + Solidity metrics failed + There was an error analyzing the scope. Try again. + + + + ) +} From 00ede6e96f297097b689478a71e787a3dd42532e Mon Sep 17 00:00:00 2001 From: Fran Rimoldi Date: Wed, 15 Nov 2023 18:11:45 +0300 Subject: [PATCH 38/80] fix: disable auto logout --- src/App.tsx | 2 +- src/AppAdmin.tsx | 15 ++------------- 2 files changed, 3 insertions(+), 14 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 35dc326e..58da7613 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -43,7 +43,7 @@ function App() { ) useEffect(() => { - if (!connectedAddress || (profile && !addressIsAllowed(connectedAddress))) { + if (profile && connectedAddress && !addressIsAllowed(connectedAddress)) { signOut() } }, [connectedAddress, addressIsAllowed, signOut, profile]) diff --git a/src/AppAdmin.tsx b/src/AppAdmin.tsx index 05d68e16..029e26aa 100644 --- a/src/AppAdmin.tsx +++ b/src/AppAdmin.tsx @@ -11,20 +11,12 @@ import { Box } from "./components/Box" import { useAdminSignIn } from "./hooks/api/admin/useAdminSignIn" import { ErrorModal } from "./pages/ContestDetails/ErrorModal" import { useAccount } from "wagmi" -import { contests as contestsAPI } from "./hooks/api/axios" -import { adminSignOut as adminSignOutUrl } from "./hooks/api/urls" const AppInternal = () => { const { data: adminAddress } = useAdminProfile() - const { address: connectedAddress, isDisconnected } = useAccount() + const { address: connectedAddress } = useAccount() const { signIn, error, reset } = useAdminSignIn() - useEffect(() => { - if (adminAddress && !connectedAddress && isDisconnected) { - contestsAPI.get(adminSignOutUrl()) - } - }, [connectedAddress, adminAddress, isDisconnected]) - const handleSignInAsAdmin = useCallback(() => { signIn() }, [signIn]) @@ -33,10 +25,7 @@ const AppInternal = () => { reset() }, [reset]) - const validAdmin = useMemo( - () => adminAddress && connectedAddress && adminAddress === connectedAddress, - [adminAddress, connectedAddress] - ) + const validAdmin = useMemo(() => !!adminAddress, [adminAddress]) const navigationLinks: NavigationLink[] = validAdmin ? [ From c49d8aa5c0255ca2e7a631f6d81d22beb5a08fb7 Mon Sep 17 00:00:00 2001 From: Fran Rimoldi Date: Wed, 22 Nov 2023 18:14:57 +0100 Subject: [PATCH 39/80] fix: Winter staking round label --- src/pages/Staking/Staking.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/Staking/Staking.tsx b/src/pages/Staking/Staking.tsx index cb4d44b1..9db79178 100644 --- a/src/pages/Staking/Staking.tsx +++ b/src/pages/Staking/Staking.tsx @@ -148,7 +148,7 @@ export const StakingPage: React.FC = () => { - The Summer 2023 Staking Round is currently open! + The Winter 2023 Staking Round is currently open! Stake From 0e429e68b8bd589010738410224c7168a22d1987 Mon Sep 17 00:00:00 2001 From: Fran Rimoldi Date: Thu, 7 Dec 2023 01:10:34 -0300 Subject: [PATCH 40/80] feat: create and confirm draft contests --- src/hooks/api/admin/useAdminConfirmContest.ts | 53 +++++++++ src/hooks/api/admin/useAdminContests.ts | 7 +- src/hooks/api/admin/useAdminCreateContest.ts | 32 +++--- src/hooks/api/urls.ts | 1 + .../AdminContestListDraft.tsx | 92 ++++++++++++++++ .../AdminContestsList/AdminContestsList.tsx | 2 + .../AdminContestsListActive.tsx | 1 + .../AdminContestsList/ConfirmContestModal.tsx | 85 +++++++++++++++ .../AdminContestsList/CreateContestForm.tsx | 102 +++++++++--------- .../AdminContestsList/CreateContestModal.tsx | 9 +- 10 files changed, 314 insertions(+), 70 deletions(-) create mode 100644 src/hooks/api/admin/useAdminConfirmContest.ts create mode 100644 src/pages/admin/AdminContestsList/AdminContestListDraft.tsx create mode 100644 src/pages/admin/AdminContestsList/ConfirmContestModal.tsx diff --git a/src/hooks/api/admin/useAdminConfirmContest.ts b/src/hooks/api/admin/useAdminConfirmContest.ts new file mode 100644 index 00000000..c3b39bfc --- /dev/null +++ b/src/hooks/api/admin/useAdminConfirmContest.ts @@ -0,0 +1,53 @@ +import { DateTime } from "luxon" +import { useMutation, useQueryClient } from "react-query" +import { contests as contestsAPI } from "../axios" +import { adminConfirmContest as adminConfirmContestUrl } from "../urls" +import { AxiosError } from "axios" +import { adminContestsQuery } from "./useAdminContests" + +type AdminConfirmContestParams = { + id: number + title: string + shortDescription: string + startDate: DateTime + endDate: DateTime + auditRewards: number + judgingPrizePool: number + leadJudgeFixedPay: number + fullPayment: number +} + +export const useAdminConfirmContest = () => { + const queryClient = useQueryClient() + const { mutate, mutateAsync, ...mutation } = useMutation( + async (params) => { + try { + await contestsAPI.post(adminConfirmContestUrl(params.id), { + title: params.title, + short_description: params.shortDescription, + starts_at: params.startDate.toSeconds(), + ends_at: params.endDate.toSeconds(), + audit_rewards: params.auditRewards, + judging_prize_pool: params.judgingPrizePool, + lead_judge_fixed_pay: params.leadJudgeFixedPay, + full_payment: params.fullPayment, + }) + } catch (error) { + const axiosError = error as AxiosError + throw Error(axiosError.response?.data.error ?? "Something went wrong. Please, try again.") + } + }, + { + onSuccess: async () => { + await queryClient.invalidateQueries(adminContestsQuery("draft")) + await queryClient.invalidateQueries(adminContestsQuery("active")) + await queryClient.invalidateQueries(adminContestsQuery("finished")) + }, + } + ) + + return { + confirmContest: mutate, + ...mutation, + } +} diff --git a/src/hooks/api/admin/useAdminContests.ts b/src/hooks/api/admin/useAdminContests.ts index 58541a1d..1bc4f5ad 100644 --- a/src/hooks/api/admin/useAdminContests.ts +++ b/src/hooks/api/admin/useAdminContests.ts @@ -3,7 +3,7 @@ import { contests as contestsAPI } from "../axios" import { getAdminContests as getAdminContestsUrl } from "../urls" -export type ContestStatus = "CREATED" | "RUNNING" | "JUDGING" | "FINISHED" | "ESCALATING" | "SHERLOCK_JUDGING" +export type ContestStatus = "DRAFT" | "CREATED" | "RUNNING" | "JUDGING" | "FINISHED" | "ESCALATING" | "SHERLOCK_JUDGING" export type ContestsListItem = { id: number @@ -29,6 +29,7 @@ export type ContestsListItem = { judgingPrizePool: number leadJudgeFixedPay: number fullPayment: number + initialScopeSubmitted: boolean } type GetAdminContestsResponse = { @@ -55,9 +56,10 @@ type GetAdminContestsResponse = { judging_prize_pool: number lead_judge_fixed_pay: number full_payment: number + initial_scope_submitted: boolean }[] -export type ContestListStatus = "active" | "finished" +export type ContestListStatus = "active" | "finished" | "draft" export const adminContestsQuery = (status: ContestListStatus) => ["admin-contests", status] export const useAdminContests = (status: ContestListStatus) => @@ -88,5 +90,6 @@ export const useAdminContests = (status: ContestListStatus) => judgingPrizePool: d.judging_prize_pool, leadJudgeFixedPay: d.lead_judge_fixed_pay, fullPayment: d.full_payment, + initialScopeSubmitted: d.initial_scope_submitted, })) }) diff --git a/src/hooks/api/admin/useAdminCreateContest.ts b/src/hooks/api/admin/useAdminCreateContest.ts index 9dcb31da..9ec2dd6d 100644 --- a/src/hooks/api/admin/useAdminCreateContest.ts +++ b/src/hooks/api/admin/useAdminCreateContest.ts @@ -16,14 +16,14 @@ type AdminCreateContestParams = { } contest: { title: string - shortDescription: string - nSLOC: string - startDate: DateTime - endDate: DateTime - auditRewards: number - judgingPrizePool: number - leadJudgeFixedPay: number - fullPayment: number + // shortDescription: string + // nSLOC: string + // startDate: DateTime + // endDate: DateTime + // auditRewards: number + // judgingPrizePool: number + // leadJudgeFixedPay: number + // fullPayment: number } } @@ -42,14 +42,14 @@ export const useAdminCreateContest = () => { website: params.protocol.website, }, title: params.contest.title, - short_description: params.contest.shortDescription, - lines_of_code: params.contest.nSLOC, - starts_at: params.contest.startDate.toSeconds(), - ends_at: params.contest.endDate.toSeconds(), - audit_rewards: params.contest.auditRewards, - judging_prize_pool: params.contest.judgingPrizePool, - lead_judge_fixed_pay: params.contest.leadJudgeFixedPay, - full_payment: params.contest.fullPayment, + // short_description: params.contest.shortDescription, + // lines_of_code: params.contest.nSLOC, + // starts_at: params.contest.startDate.toSeconds(), + // ends_at: params.contest.endDate.toSeconds(), + // audit_rewards: params.contest.auditRewards, + // judging_prize_pool: params.contest.judgingPrizePool, + // lead_judge_fixed_pay: params.contest.leadJudgeFixedPay, + // full_payment: params.contest.fullPayment, }) } catch (error) { const axiosError = error as AxiosError diff --git a/src/hooks/api/urls.ts b/src/hooks/api/urls.ts index 531db8af..6897351f 100644 --- a/src/hooks/api/urls.ts +++ b/src/hooks/api/urls.ts @@ -68,6 +68,7 @@ export const getAdminProtocol = (name: string) => `/admin/protocol/${name}` export const getAdminContestScope = (contestID: number) => `/admin/contest/${contestID}/scope` export const adminCreateContest = () => `/admin/contests` export const adminUpdateContest = (contestID: number) => `/admin/contests/${contestID}` +export const adminConfirmContest = (contestID: number) => `/admin/contests/${contestID}/confirm` export const adminResetScope = (contestID: number) => `/admin/contests/${contestID}/scope` export const getAdminTwitterAccount = (handle: string) => `/admin/twitter_account/${handle}` export const adminSubmitScope = () => `/admin/scope` diff --git a/src/pages/admin/AdminContestsList/AdminContestListDraft.tsx b/src/pages/admin/AdminContestsList/AdminContestListDraft.tsx new file mode 100644 index 00000000..a2988b8d --- /dev/null +++ b/src/pages/admin/AdminContestsList/AdminContestListDraft.tsx @@ -0,0 +1,92 @@ +import { useState } from "react" +import { FaClipboardList } from "react-icons/fa" +import { Box } from "../../../components/Box" +import { Button } from "../../../components/Button" +import { Column, Row } from "../../../components/Layout" +import LoadingContainer from "../../../components/LoadingContainer/LoadingContainer" +import { Table, TBody, Td, Th, THead, Tr } from "../../../components/Table/Table" +import { Text } from "../../../components/Text" +import { Title } from "../../../components/Title" +import { useAdminContests } from "../../../hooks/api/admin/useAdminContests" + +import styles from "./AdminContestsList.module.scss" +import { ConfirmContestModal } from "./ConfirmContestModal" + +export const AdminContestListDraft = () => { + const { data: contests, isLoading } = useAdminContests("draft") + + const [confirmContestIndex, setConfirmContestIndex] = useState() + + return ( + + + + DRAFTS + + + + + + + + + + + + {contests?.map((c, index) => { + return ( + + + + + + + + ) + })} + +
+ ID + + Contest + StatusAction
{c.id} + + {c.title} + {c.title} + + + + + + {c.initialScopeSubmitted ? "Initial scope submitted" : "Waiting on initial scope"} + + + + + +
+ {confirmContestIndex !== undefined && contests && ( + setConfirmContestIndex(undefined)} + contest={contests[confirmContestIndex]} + /> + )} +
+
+
+ ) +} diff --git a/src/pages/admin/AdminContestsList/AdminContestsList.tsx b/src/pages/admin/AdminContestsList/AdminContestsList.tsx index 483fea5b..16240a10 100644 --- a/src/pages/admin/AdminContestsList/AdminContestsList.tsx +++ b/src/pages/admin/AdminContestsList/AdminContestsList.tsx @@ -1,10 +1,12 @@ import { Column } from "../../../components/Layout" +import { AdminContestListDraft } from "./AdminContestListDraft" import { AdminContestsListActive } from "./AdminContestsListActive" import { AdminContestsListFinished } from "./AdminContestsListFinished" export const AdminContestsList = () => { return ( + diff --git a/src/pages/admin/AdminContestsList/AdminContestsListActive.tsx b/src/pages/admin/AdminContestsList/AdminContestsListActive.tsx index c5f7427a..d9953d5a 100644 --- a/src/pages/admin/AdminContestsList/AdminContestsListActive.tsx +++ b/src/pages/admin/AdminContestsList/AdminContestsListActive.tsx @@ -30,6 +30,7 @@ type ContestLifeCycleStatus = | "ESCALATING" | "SHERLOCK_JUDGING" | "FINISHED" + | "DRAFT" const getCurrentStatus = (contest: ContestsListItem): ContestLifeCycleStatus => { if (!contest.initialPayment) return "WAITING_INITIAL_PAYMENT" diff --git a/src/pages/admin/AdminContestsList/ConfirmContestModal.tsx b/src/pages/admin/AdminContestsList/ConfirmContestModal.tsx new file mode 100644 index 00000000..f771a12a --- /dev/null +++ b/src/pages/admin/AdminContestsList/ConfirmContestModal.tsx @@ -0,0 +1,85 @@ +import { useCallback, useEffect, useState } from "react" +import { Button } from "../../../components/Button" +import { Column, Row } from "../../../components/Layout" +import LoadingContainer from "../../../components/LoadingContainer/LoadingContainer" +import Modal, { Props as ModalProps } from "../../../components/Modal/Modal" +import { Text } from "../../../components/Text" +import { Title } from "../../../components/Title" +import { useAdminConfirmContest } from "../../../hooks/api/admin/useAdminConfirmContest" +import { ContestsListItem } from "../../../hooks/api/admin/useAdminContests" +import { ErrorModal } from "../../ContestDetails/ErrorModal" +import { ContestValues, CreateContestForm } from "./CreateContestForm" + +type Props = ModalProps & { + contest: ContestsListItem +} + +export const ConfirmContestModal: React.FC = ({ onClose, contest }) => { + const [formIsDirty, setFormIsDirty] = useState(false) + const [displayModalCloseConfirm, setDisplayModalFormConfirm] = useState(false) + + const { confirmContest, isSuccess, isLoading, error, reset } = useAdminConfirmContest() + + useEffect(() => { + if (isSuccess) onClose?.() + }, [isSuccess, onClose]) + + const handleModalClose = useCallback(() => { + if (formIsDirty) { + setDisplayModalFormConfirm(true) + } else { + onClose && onClose() + } + }, [setDisplayModalFormConfirm, onClose, formIsDirty]) + + const handleModalCloseConfirm = useCallback(() => { + onClose && onClose() + }, [onClose]) + + const handleModalCloseCancel = useCallback(() => { + setDisplayModalFormConfirm(false) + }, []) + + const handleFormSubmit = useCallback( + (values: ContestValues) => { + confirmContest({ + id: contest.id, + ...values.contest, + }) + }, + [contest.id, confirmContest] + ) + return ( + + {displayModalCloseConfirm && ( + + + Unsaved contest + + Are you sure you want to close this form? All unsaved changes will be lost and you will need to start + over. + + + + + + + + )} + + + New contest + + + + {error && reset()} />} + + ) +} diff --git a/src/pages/admin/AdminContestsList/CreateContestForm.tsx b/src/pages/admin/AdminContestsList/CreateContestForm.tsx index 5cb4d476..aeed6c5e 100644 --- a/src/pages/admin/AdminContestsList/CreateContestForm.tsx +++ b/src/pages/admin/AdminContestsList/CreateContestForm.tsx @@ -44,11 +44,18 @@ type Props = { onDirtyChange: (dirty: boolean) => void submitLabel: string contest?: ContestsListItem + draft?: boolean } const DATE_FORMAT = "yyyy-MM-dd" -export const CreateContestForm: React.FC = ({ onSubmit, onDirtyChange, submitLabel, contest }) => { +export const CreateContestForm: React.FC = ({ + onSubmit, + onDirtyChange, + submitLabel, + contest, + draft = false, +}) => { const [protocolName, setProtocolName] = useState("") const [debouncedProtocolName] = useDebounce(protocolName, 300) @@ -114,7 +121,7 @@ export const CreateContestForm: React.FC = ({ onSubmit, onDirtyChange, su }, [contest]) useEffect(() => { - if (contestVariablesSuccess && !contest) { + if (contestVariablesSuccess && (!contest || contest.status === "DRAFT")) { setContestAuditLength(`${contestVariables.length}`) setInitialTotalRewards( ethers.utils.parseUnits( @@ -195,7 +202,10 @@ export const CreateContestForm: React.FC = ({ onSubmit, onDirtyChange, su } if (contestTitle === "") return false - if (contestShortDescription.length < 100 || contestShortDescription.length > 200) return false + + if (draft) return true + + if (contestShortDescription === "") return false if (contestAuditLength === "") return false const startDate = DateTime.fromFormat(contestStartDate, DATE_FORMAT) @@ -271,20 +281,6 @@ export const CreateContestForm: React.FC = ({ onSubmit, onDirtyChange, su protocolName, ]) - const handleUpdateShortDescription = useCallback((value: string) => { - setShortDescription(value) - - if (value === "") { - setShortDescriptionError(undefined) - } else if (value.length < 100) { - setShortDescriptionError("Too short. Must be between 100 and 200 characters.") - } else if (value.length > 200) { - setShortDescriptionError("Too long. Must be between 100 and 200 characters.") - } else { - setShortDescriptionError(undefined) - } - }, []) - const handleSubmit = useCallback(() => { const startDate = DateTime.fromFormat(contestStartDate, DATE_FORMAT, { zone: "utc" }).set({ hour: 15, @@ -418,39 +414,45 @@ export const CreateContestForm: React.FC = ({ onSubmit, onDirtyChange, su - - - - - - - - - - - - - - - - - - - - - - - - - - - - {`Admin Fee: ${sherlockFee} USDC`} + {draft ? null : ( + <> + + + + + + + + + + + + + + + + + + + + + + + + + + + + {`Admin Fee: ${sherlockFee} USDC`} + + )}

+ + + + + @@ -85,6 +98,12 @@ export const AdminContestListDraft = () => { contest={contests[confirmContestIndex]} /> )} + {resetContestIndex !== undefined && contests && ( + setResetContestIndex(undefined)} + contest={contests[resetContestIndex]} + /> + )}
diff --git a/src/pages/admin/AdminContestsList/ContestResetInitialScopeModal.tsx b/src/pages/admin/AdminContestsList/ContestResetInitialScopeModal.tsx new file mode 100644 index 00000000..72a67eaf --- /dev/null +++ b/src/pages/admin/AdminContestsList/ContestResetInitialScopeModal.tsx @@ -0,0 +1,49 @@ +import { useCallback, useEffect } from "react" +import { Button } from "../../../components/Button" +import { Column, Row } from "../../../components/Layout" +import LoadingContainer from "../../../components/LoadingContainer/LoadingContainer" +import Modal, { Props as ModalProps } from "../../../components/Modal/Modal" +import { Text } from "../../../components/Text" +import { Title } from "../../../components/Title" +import { ContestsListItem } from "../../../hooks/api/admin/useAdminContests" +import { useAdminResetScope } from "../../../hooks/api/admin/useAdminResetScope" + +type Props = { + contest: ContestsListItem +} & ModalProps + +export const ContestResetInitialScopeModal: React.FC = ({ onClose, contest }) => { + const { resetScope, isLoading, isSuccess } = useAdminResetScope() + + useEffect(() => { + if (isSuccess) { + onClose?.() + } + }, [isSuccess, onClose]) + + const handleResetScope = useCallback(() => { + resetScope({ + contestID: contest.id, + scopeType: "initial", + }) + }, [resetScope, contest.id]) + + return ( + + + + {contest.title}: Reset initial scope + Are you sure you want to reset the scope for this contest? + + + + + + + + ) +} diff --git a/src/pages/admin/AdminContestsList/ContestScopeModal.tsx b/src/pages/admin/AdminContestsList/ContestScopeModal.tsx index 70964679..c5722ff2 100644 --- a/src/pages/admin/AdminContestsList/ContestScopeModal.tsx +++ b/src/pages/admin/AdminContestsList/ContestScopeModal.tsx @@ -100,7 +100,7 @@ export const ContestScopeModal: React.FC = ({ onClose, contestID }) => { {index < scope.length - 1 ?
: null}
))} -
From 12c322d6eae0a3a7c837b656726ab7c2c2f9e383 Mon Sep 17 00:00:00 2001 From: Fran Rimoldi Date: Wed, 13 Dec 2023 19:17:28 -0300 Subject: [PATCH 44/80] feat: delete draft contest --- .../api/admin/useAdminDeleteDraftContest.ts | 29 +++++++++++ src/hooks/api/urls.ts | 1 + .../AdminContestListDraft.tsx | 13 ++++- .../DeleteDraftContestModal.tsx | 48 +++++++++++++++++++ 4 files changed, 90 insertions(+), 1 deletion(-) create mode 100644 src/hooks/api/admin/useAdminDeleteDraftContest.ts create mode 100644 src/pages/admin/AdminContestsList/DeleteDraftContestModal.tsx diff --git a/src/hooks/api/admin/useAdminDeleteDraftContest.ts b/src/hooks/api/admin/useAdminDeleteDraftContest.ts new file mode 100644 index 00000000..ec518a1f --- /dev/null +++ b/src/hooks/api/admin/useAdminDeleteDraftContest.ts @@ -0,0 +1,29 @@ +import { useMutation } from "wagmi" +import { adminDeleteDraftContest as adminDeleteDraftContestUrl } from "../urls" +import { contests as contestsAPI } from "../axios" +import { useQueryClient } from "react-query" +import { adminContestsQuery } from "./useAdminContests" + +type AdminDeleteDraftContestParams = { + contestID: number +} + +export const useAdminDeleteDraftContest = () => { + const queryClient = useQueryClient() + + const { mutate, mutateAsync, ...mutation } = useMutation( + async (params) => { + await contestsAPI.delete(adminDeleteDraftContestUrl(params.contestID)) + }, + { + async onSettled(data, error, params) { + await queryClient.invalidateQueries(adminContestsQuery("draft")) + }, + } + ) + + return { + deleteContest: mutate, + ...mutation, + } +} diff --git a/src/hooks/api/urls.ts b/src/hooks/api/urls.ts index 578d6490..c3fe155f 100644 --- a/src/hooks/api/urls.ts +++ b/src/hooks/api/urls.ts @@ -78,6 +78,7 @@ export const adminStartLeadSeniorWatsonSelection = () => `/admin/start_lead_seni export const adminSelectLeadSeniorWatson = () => `/admin/select_lead_senior_watson` export const adminGenerateReport = (contestID: number) => `/admin/contest/${contestID}/report/generate` export const adminPublishReport = (contestID: number) => `/admin/contest/${contestID}/report/publish` +export const adminDeleteDraftContest = (contestID: number) => `/admin/contests/${contestID}` // Stats export const getLeaderboard = () => "/stats/leaderboard" diff --git a/src/pages/admin/AdminContestsList/AdminContestListDraft.tsx b/src/pages/admin/AdminContestsList/AdminContestListDraft.tsx index 2828029c..f0b4aaad 100644 --- a/src/pages/admin/AdminContestsList/AdminContestListDraft.tsx +++ b/src/pages/admin/AdminContestsList/AdminContestListDraft.tsx @@ -1,5 +1,5 @@ import { useState } from "react" -import { FaClipboardList, FaRecycle, FaRedo, FaUndo } from "react-icons/fa" +import { FaClipboardList, FaRecycle, FaRedo, FaTrash, FaUndo } from "react-icons/fa" import { Box } from "../../../components/Box" import { Button } from "../../../components/Button" import { Column, Row } from "../../../components/Layout" @@ -12,12 +12,14 @@ import { useAdminContests } from "../../../hooks/api/admin/useAdminContests" import styles from "./AdminContestsList.module.scss" import { ConfirmContestModal } from "./ConfirmContestModal" import { ContestResetInitialScopeModal } from "./ContestResetInitialScopeModal" +import { DeleteDraftContestModal } from "./DeleteDraftContestModal" export const AdminContestListDraft = () => { const { data: contests, isLoading } = useAdminContests("draft") const [confirmContestIndex, setConfirmContestIndex] = useState() const [resetContestIndex, setResetContestIndex] = useState() + const [deleteContestIndex, setDeleteContestIndex] = useState() return ( @@ -68,6 +70,9 @@ export const AdminContestListDraft = () => { > + @@ -104,6 +109,12 @@ export const AdminContestListDraft = () => { contest={contests[resetContestIndex]} /> )} + {deleteContestIndex !== undefined && contests && ( + setDeleteContestIndex(undefined)} + contest={contests[deleteContestIndex]} + /> + )} diff --git a/src/pages/admin/AdminContestsList/DeleteDraftContestModal.tsx b/src/pages/admin/AdminContestsList/DeleteDraftContestModal.tsx new file mode 100644 index 00000000..2a5ba09a --- /dev/null +++ b/src/pages/admin/AdminContestsList/DeleteDraftContestModal.tsx @@ -0,0 +1,48 @@ +import { useCallback, useEffect } from "react" +import { Button } from "../../../components/Button" +import { Column, Row } from "../../../components/Layout" +import LoadingContainer from "../../../components/LoadingContainer/LoadingContainer" +import Modal, { Props as ModalProps } from "../../../components/Modal/Modal" +import { Text } from "../../../components/Text" +import { Title } from "../../../components/Title" +import { ContestsListItem } from "../../../hooks/api/admin/useAdminContests" +import { useAdminDeleteDraftContest } from "../../../hooks/api/admin/useAdminDeleteDraftContest" + +type Props = { + contest: ContestsListItem +} & ModalProps + +export const DeleteDraftContestModal: React.FC = ({ onClose, contest }) => { + const { deleteContest, isLoading, isSuccess } = useAdminDeleteDraftContest() + + useEffect(() => { + if (isSuccess) { + onClose?.() + } + }, [isSuccess, onClose]) + + const handleResetScope = useCallback(() => { + deleteContest({ + contestID: contest?.id, + }) + }, [deleteContest, contest]) + + return ( + + + + {contest?.title}: Delete contest + Are you sure you want to delete this draft contest? + + + + + + + + ) +} From 31002f91581b19e901ecf5abb050738a528af855 Mon Sep 17 00:00:00 2001 From: Rares Stanciu Date: Thu, 14 Dec 2023 23:15:54 +0200 Subject: [PATCH 45/80] feat: dashboard redirect --- vercel.json | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/vercel.json b/vercel.json index 2c05711d..7e11211a 100644 --- a/vercel.json +++ b/vercel.json @@ -3,12 +3,17 @@ { "source": "/audits", "destination": "https://audits.sherlock.xyz", - "permanent": false + "permanent": true }, { "source": "/audits/:path*", "destination": "https://audits.sherlock.xyz/:path*", - "permanent": false + "permanent": true + }, + { + "source": "/dashboard/:path*", + "destination": "https://audits.sherlock.xyz/dashboard/:path*", + "permanent": true } ] } \ No newline at end of file From 6f5aff912b40ce0f043121c7243bf6c30adf25a2 Mon Sep 17 00:00:00 2001 From: Fran Rimoldi Date: Tue, 19 Dec 2023 09:50:51 -0300 Subject: [PATCH 46/80] fix: remove submission ready condition --- .../admin/AdminContestsList/AdminContestsListActive.tsx | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/pages/admin/AdminContestsList/AdminContestsListActive.tsx b/src/pages/admin/AdminContestsList/AdminContestsListActive.tsx index 86f5a54f..d8b2a970 100644 --- a/src/pages/admin/AdminContestsList/AdminContestsListActive.tsx +++ b/src/pages/admin/AdminContestsList/AdminContestsListActive.tsx @@ -22,7 +22,6 @@ type ContestLifeCycleStatus = | "WAITING_FOR_SENIOR_SELECTION" | "READY_TO_PUBLISH" | "WAITING_ON_FINAL_PAYMENT" - | "WAITING_ON_FINALIZE_SUBMISSION" | "READY_TO_APPROVE_START" | "START_APPROVED" | "RUNNING" @@ -38,7 +37,6 @@ const getCurrentStatus = (contest: ContestsListItem): ContestLifeCycleStatus => if (!contest.leadSeniorAuditorHandle) return "WAITING_FOR_SENIOR_SELECTION" if (!contest.adminUpcomingApproved) return "READY_TO_PUBLISH" if (!contest.fullPaymentComplete) return "WAITING_ON_FINAL_PAYMENT" - if (!contest.submissionReady) return "WAITING_ON_FINALIZE_SUBMISSION" if (!contest.adminStartApproved) return "READY_TO_APPROVE_START" switch (contest.status) { @@ -200,9 +198,6 @@ export const AdminContestsListActive = () => { return Waiting on full payment } - if (status === "WAITING_ON_FINALIZE_SUBMISSION") - return Waiting of protocol to finalize submission - if (status === "READY_TO_APPROVE_START") { return Ready to approve start } From efb7625366bd684fa0b9620e0de9c0214cafae63 Mon Sep 17 00:00:00 2001 From: Fran Rimoldi Date: Fri, 22 Dec 2023 10:58:04 -0300 Subject: [PATCH 47/80] fix: scoping --- src/hooks/api/admin/useAdminContest.ts | 28 ++++++++ src/hooks/api/admin/useAdminContests.ts | 69 ++++++++++--------- src/hooks/api/scope/useScope.ts | 1 + src/hooks/api/urls.ts | 1 + .../AdminContestsList/ContestScopeModal.tsx | 42 +++++------ .../AdminContestsList/CreateContestForm.tsx | 4 +- 6 files changed, 91 insertions(+), 54 deletions(-) create mode 100644 src/hooks/api/admin/useAdminContest.ts diff --git a/src/hooks/api/admin/useAdminContest.ts b/src/hooks/api/admin/useAdminContest.ts new file mode 100644 index 00000000..8993e09f --- /dev/null +++ b/src/hooks/api/admin/useAdminContest.ts @@ -0,0 +1,28 @@ +import { useQuery } from "react-query" +import { contests as contestsAPI } from "../axios" +import { getAdminContest as getAdminContestUrl } from "../urls" +import { ContestsListItem, GetAdminContestsResponse, parseContest } from "./useAdminContests" + +type AdminContestResponse = { + contest: GetAdminContestsResponse & { + nsloc?: number + expected_nsloc?: number + } +} + +type AdminContest = ContestsListItem & { + nSLOC?: number + expectedNSLOC?: number +} + +export const adminContestQuery = (contestID: number) => ["admin-contest", contestID] +export const useAdminContest = (contestID: number) => + useQuery(adminContestQuery(contestID), async () => { + const { data } = await contestsAPI.get(getAdminContestUrl(contestID)) + + return { + ...parseContest(data.contest), + nSLOC: data.contest.nsloc, + expectedNSLOC: data.contest.expected_nsloc, + } + }) diff --git a/src/hooks/api/admin/useAdminContests.ts b/src/hooks/api/admin/useAdminContests.ts index 0503c443..3c911767 100644 --- a/src/hooks/api/admin/useAdminContests.ts +++ b/src/hooks/api/admin/useAdminContests.ts @@ -24,7 +24,8 @@ export type ContestsListItem = { leadSeniorSelectionMessageSentAt: number leadSeniorConfirmationMessage: string auditReport?: string - linesOfCode?: string + nSLOC?: number + expectedNSLOC?: number rewards: number judgingPrizePool: number leadJudgeFixedPay: number @@ -33,7 +34,7 @@ export type ContestsListItem = { finalScopeSubmitted: boolean } -type GetAdminContestsResponse = { +export type GetAdminContestsResponse = { id: number title: string short_description: string @@ -52,47 +53,51 @@ type GetAdminContestsResponse = { senior_selection_message_sent_at: number senior_confirmed_message: string audit_report?: string - lines_of_code?: string + nsloc?: number + expected_nsloc?: number audit_rewards: number judging_prize_pool: number lead_judge_fixed_pay: number full_payment: number initial_scope_submitted: boolean final_scope_submitted: boolean -}[] +} export type ContestListStatus = "active" | "finished" | "draft" +export const parseContest = (d: GetAdminContestsResponse): ContestsListItem => { + return { + id: d.id, + title: d.title, + shortDescription: d.short_description, + logoURL: d.logo_url, + status: d.status, + initialPayment: d.initial_payment_complete, + fullPaymentComplete: d.full_payment_complete, + adminUpcomingApproved: d.admin_upcoming_approved, + adminStartApproved: d.admin_start_approved, + dashboardID: d.dashboard_id, + startDate: d.starts_at, + endDate: d.ends_at, + submissionReady: d.protocol_submission_ready, + hasSolidityMetricsReport: d.has_solidity_metrics_report, + leadSeniorAuditorHandle: d.lead_senior_auditor_handle, + leadSeniorSelectionMessageSentAt: d.senior_selection_message_sent_at, + leadSeniorConfirmationMessage: d.senior_confirmed_message, + auditReport: d.audit_report, + rewards: d.audit_rewards, + judgingPrizePool: d.judging_prize_pool, + leadJudgeFixedPay: d.lead_judge_fixed_pay, + fullPayment: d.full_payment, + initialScopeSubmitted: d.initial_scope_submitted, + finalScopeSubmitted: d.final_scope_submitted, + } +} + export const adminContestsQuery = (status: ContestListStatus) => ["admin-contests", status] export const useAdminContests = (status: ContestListStatus) => useQuery(adminContestsQuery(status), async () => { - const { data } = await contestsAPI.get(getAdminContestsUrl(status)) + const { data } = await contestsAPI.get(getAdminContestsUrl(status)) - return data.map((d) => ({ - id: d.id, - title: d.title, - shortDescription: d.short_description, - logoURL: d.logo_url, - status: d.status, - initialPayment: d.initial_payment_complete, - fullPaymentComplete: d.full_payment_complete, - adminUpcomingApproved: d.admin_upcoming_approved, - adminStartApproved: d.admin_start_approved, - dashboardID: d.dashboard_id, - startDate: d.starts_at, - endDate: d.ends_at, - submissionReady: d.protocol_submission_ready, - hasSolidityMetricsReport: d.has_solidity_metrics_report, - leadSeniorAuditorHandle: d.lead_senior_auditor_handle, - leadSeniorSelectionMessageSentAt: d.senior_selection_message_sent_at, - leadSeniorConfirmationMessage: d.senior_confirmed_message, - auditReport: d.audit_report, - linesOfCode: d.lines_of_code, - rewards: d.audit_rewards, - judgingPrizePool: d.judging_prize_pool, - leadJudgeFixedPay: d.lead_judge_fixed_pay, - fullPayment: d.full_payment, - initialScopeSubmitted: d.initial_scope_submitted, - finalScopeSubmitted: d.final_scope_submitted, - })) + return data.map(parseContest) }) diff --git a/src/hooks/api/scope/useScope.ts b/src/hooks/api/scope/useScope.ts index 2e912d10..739edf4f 100644 --- a/src/hooks/api/scope/useScope.ts +++ b/src/hooks/api/scope/useScope.ts @@ -14,6 +14,7 @@ export type Scope = { solidityMetricsReport?: string commentToSourceRatio?: number initialScope?: Scope + nSLOC?: number } type ScopeResponse = { diff --git a/src/hooks/api/urls.ts b/src/hooks/api/urls.ts index c3fe155f..06623fce 100644 --- a/src/hooks/api/urls.ts +++ b/src/hooks/api/urls.ts @@ -79,6 +79,7 @@ export const adminSelectLeadSeniorWatson = () => `/admin/select_lead_senior_wats export const adminGenerateReport = (contestID: number) => `/admin/contest/${contestID}/report/generate` export const adminPublishReport = (contestID: number) => `/admin/contest/${contestID}/report/publish` export const adminDeleteDraftContest = (contestID: number) => `/admin/contests/${contestID}` +export const getAdminContest = (contestID: number) => `/admin/contests/${contestID}` // Stats export const getLeaderboard = () => "/stats/leaderboard" diff --git a/src/pages/admin/AdminContestsList/ContestScopeModal.tsx b/src/pages/admin/AdminContestsList/ContestScopeModal.tsx index c5722ff2..bcdee7fc 100644 --- a/src/pages/admin/AdminContestsList/ContestScopeModal.tsx +++ b/src/pages/admin/AdminContestsList/ContestScopeModal.tsx @@ -12,6 +12,7 @@ import { useContest } from "../../../hooks/api/contests" import styles from "./AdminContestsList.module.scss" import { useAdminResetScope } from "../../../hooks/api/admin/useAdminResetScope" +import { useAdminContest } from "../../../hooks/api/admin/useAdminContest" type Props = ModalProps & { contestID: number @@ -20,12 +21,11 @@ type Props = ModalProps & { const COMMENT_TO_SOURCE_MIN = 0.8 export const ContestScopeModal: React.FC = ({ onClose, contestID }) => { - const { data: contest, isLoading: contestIsLoading } = useContest(contestID) + const { data: contest, isLoading: contestIsLoading } = useAdminContest(contestID) const { data: scope, isLoading: scopeIsLoading } = useAdminContestScope(contestID) const { resetScope, isLoading: resetScopeIsLoading } = useAdminResetScope() - const submittedNSLOC = scope?.reduce((t, s) => t + (s.files.reduce((t, f) => t + (f.nSLOC ?? 0), 0) ?? 0), 0) ?? 0 - const expectedNSLOCExceeded = contest && scope && (parseInt(contest.linesOfCode ?? "") ?? 0) < (submittedNSLOC ?? 0) + const expectedNSLOCExceeded = contest?.nSLOC && contest.expectedNSLOC && contest.nSLOC > contest.expectedNSLOC return ( @@ -39,10 +39,10 @@ export const ContestScopeModal: React.FC = ({ onClose, contestID }) => { Submitted nSLOC is higher than expected - Expected nSLOC: {contest?.linesOfCode} + Expected nSLOC: {contest?.expectedNSLOC} - Submitted nSLOC: {submittedNSLOC} + Submitted nSLOC: {contest.nSLOC} ) : null} @@ -70,24 +70,26 @@ export const ContestScopeModal: React.FC = ({ onClose, contestID }) => { nSLOC: - {s.files.reduce((t, f) => t + (f.nSLOC ?? 0), 0)} - - - - - - - Comments ratio: - {Math.round(s.commentToSourceRatio! * 100)}% - {s.commentToSourceRatio! < COMMENT_TO_SOURCE_MIN ? ( - - - Comments ratio is below 80% - - ) : null} + {s.nSLOC} + {s.commentToSourceRatio ? ( + + + + Comments ratio: + {Math.round(s.commentToSourceRatio * 100)}% + {s.commentToSourceRatio < COMMENT_TO_SOURCE_MIN ? ( + + + Comments ratio is below 80% + + ) : null} + + + + ) : null} + + + From cdb35cd0b30c4fcb0f8c32302bc213206eb4f2b4 Mon Sep 17 00:00:00 2001 From: Rares Stanciu Date: Wed, 17 Jan 2024 14:17:13 +0200 Subject: [PATCH 52/80] feat: redirect --- vercel.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/vercel.json b/vercel.json index 7e11211a..def3bd86 100644 --- a/vercel.json +++ b/vercel.json @@ -14,6 +14,11 @@ "source": "/dashboard/:path*", "destination": "https://audits.sherlock.xyz/dashboard/:path*", "permanent": true + }, + { + "source": "/admin/payouts", + "destination": "https://audits.sherlock.xyz/admin/payouts", + "permanent": true } ] -} \ No newline at end of file +} From 43c0622759862639004bfe6b6a00edaaeb5391bd Mon Sep 17 00:00:00 2001 From: Fran Rimoldi Date: Wed, 17 Jan 2024 13:23:47 -0300 Subject: [PATCH 53/80] feat: add payouts nav link to admin --- src/AppAdmin.tsx | 4 ---- src/components/Header/Header.tsx | 3 ++- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/AppAdmin.tsx b/src/AppAdmin.tsx index 029e26aa..ef58c055 100644 --- a/src/AppAdmin.tsx +++ b/src/AppAdmin.tsx @@ -37,10 +37,6 @@ const AppInternal = () => { title: "CONTESTS", route: adminRoutes.Contests, }, - { - title: "SCOPE", - route: adminRoutes.Scope, - }, ] : [] diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx index c685c4e9..a4df8c14 100644 --- a/src/components/Header/Header.tsx +++ b/src/components/Header/Header.tsx @@ -16,7 +16,7 @@ import { Title } from "../Title" export type NavigationLink = { title: string - route: Route + route: Route | string external?: boolean protected?: boolean } @@ -104,6 +104,7 @@ export const Header: React.FC = ({ {navLink.external && } ))} + PAYOUTS )} From 35f54705d7dad477c5f749b41e28912594db53b0 Mon Sep 17 00:00:00 2001 From: Fran Rimoldi Date: Thu, 18 Jan 2024 09:56:52 -0300 Subject: [PATCH 54/80] fix: navigation payouts --- src/AppAdmin.tsx | 2 +- src/components/Header/Header.tsx | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/AppAdmin.tsx b/src/AppAdmin.tsx index ef58c055..7646369f 100644 --- a/src/AppAdmin.tsx +++ b/src/AppAdmin.tsx @@ -43,7 +43,7 @@ const AppInternal = () => { return (
-
+
{validAdmin ? ( diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx index a4df8c14..55e3442b 100644 --- a/src/components/Header/Header.tsx +++ b/src/components/Header/Header.tsx @@ -48,6 +48,8 @@ type HeaderProps = { * URL of logo. Default to Sherlock's */ logoURL?: string + + includePayouts?: boolean } /** @@ -60,6 +62,7 @@ export const Header: React.FC = ({ connectButton = true, title, logoURL, + includePayouts, }) => { const { authenticate } = useAuthentication() const { data: authenticatedProfile, isFetched } = useProfile() @@ -104,7 +107,7 @@ export const Header: React.FC = ({ {navLink.external && } ))} - PAYOUTS + {includePayouts ? PAYOUTS : null}
)} From f5f564d5621402f5a733978c31fbd44db879da20 Mon Sep 17 00:00:00 2001 From: Fran Rimoldi Date: Fri, 19 Jan 2024 17:30:49 -0300 Subject: [PATCH 55/80] feat: display multiple tweets in announcement preview --- .../api/admin/useAdminContestTweetPreview.ts | 6 ++--- .../ContestAnnouncementTweetPreview.tsx | 26 ++++++++++++------- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/src/hooks/api/admin/useAdminContestTweetPreview.ts b/src/hooks/api/admin/useAdminContestTweetPreview.ts index 4020155f..f3e3858d 100644 --- a/src/hooks/api/admin/useAdminContestTweetPreview.ts +++ b/src/hooks/api/admin/useAdminContestTweetPreview.ts @@ -4,8 +4,8 @@ import { getAdminContestTweetPreview as getAdminContestTweetPreviewUrl } from ". export const adminContestTweetPreviewKey = (contestID: number) => ["contest-tweet-preview", contestID] export const useAdminContestTweetPreview = (contestID: number) => - useQuery(adminContestTweetPreviewKey(contestID), async () => { - const { data } = await contestsAPI.get(getAdminContestTweetPreviewUrl(contestID)) + useQuery(adminContestTweetPreviewKey(contestID), async () => { + const { data } = await contestsAPI.get<{ tweets: string[] }>(getAdminContestTweetPreviewUrl(contestID)) - return data + return data.tweets }) diff --git a/src/pages/admin/AdminContestsList/ContestAnnouncementTweetPreview.tsx b/src/pages/admin/AdminContestsList/ContestAnnouncementTweetPreview.tsx index a9150c00..a3602ae7 100644 --- a/src/pages/admin/AdminContestsList/ContestAnnouncementTweetPreview.tsx +++ b/src/pages/admin/AdminContestsList/ContestAnnouncementTweetPreview.tsx @@ -1,4 +1,5 @@ import TweetCard from "react-tweet-card" +import { Column, Row } from "../../../components/Layout" import { useAdminContestTweetPreview } from "../../../hooks/api/admin/useAdminContestTweetPreview" type Props = { @@ -6,17 +7,22 @@ type Props = { } export const ContestAnnouncementTweetPreview: React.FC = ({ contestID }) => { - const { data: tweet } = useAdminContestTweetPreview(contestID) + const { data: tweets } = useAdminContestTweetPreview(contestID) return ( - + + {tweets?.map((tweet) => ( + + ))} + ) } From 864b945afa6047aaf3211908bda67916a06a6207 Mon Sep 17 00:00:00 2001 From: Fran Rimoldi Date: Mon, 22 Jan 2024 16:33:51 -0300 Subject: [PATCH 56/80] feat: display indicator if telegram is added --- src/hooks/api/admin/useAdminContests.ts | 3 +++ .../AdminContestsList.module.scss | 10 ++++++++++ .../AdminContestsListActive.tsx | 2 ++ .../AdminContestsList/TelegramBotIndicator.tsx | 17 +++++++++++++++++ 4 files changed, 32 insertions(+) create mode 100644 src/pages/admin/AdminContestsList/TelegramBotIndicator.tsx diff --git a/src/hooks/api/admin/useAdminContests.ts b/src/hooks/api/admin/useAdminContests.ts index 12fffc3d..27d3683e 100644 --- a/src/hooks/api/admin/useAdminContests.ts +++ b/src/hooks/api/admin/useAdminContests.ts @@ -32,6 +32,7 @@ export type ContestsListItem = { fullPayment: number initialScopeSubmitted: boolean finalScopeSubmitted: boolean + telegramChat?: string } export type GetAdminContestsResponse = { @@ -61,6 +62,7 @@ export type GetAdminContestsResponse = { full_payment: number initial_scope_submitted: boolean final_scope_submitted: boolean + telegram_chat?: string } export type ContestListStatus = "active" | "finished" | "draft" @@ -92,6 +94,7 @@ export const parseContest = (d: GetAdminContestsResponse): ContestsListItem => { initialScopeSubmitted: d.initial_scope_submitted, finalScopeSubmitted: d.final_scope_submitted, nSLOC: d.nsloc, + telegramChat: d.telegram_chat, } } diff --git a/src/pages/admin/AdminContestsList/AdminContestsList.module.scss b/src/pages/admin/AdminContestsList/AdminContestsList.module.scss index 43b8f8f8..bab23a7f 100644 --- a/src/pages/admin/AdminContestsList/AdminContestsList.module.scss +++ b/src/pages/admin/AdminContestsList/AdminContestsList.module.scss @@ -76,3 +76,13 @@ background-color: transparentize($color: #ffcc00, $amount: 0.9); color: #ffcc00; } + +.telegram { + opacity: 0.6; + display: flex; + font-weight: bold; + + & *:first-child { + margin-right: 5px; + } +} diff --git a/src/pages/admin/AdminContestsList/AdminContestsListActive.tsx b/src/pages/admin/AdminContestsList/AdminContestsListActive.tsx index d8b2a970..820b8c75 100644 --- a/src/pages/admin/AdminContestsList/AdminContestsListActive.tsx +++ b/src/pages/admin/AdminContestsList/AdminContestsListActive.tsx @@ -15,6 +15,7 @@ import { ConfirmContestActionModal } from "./ConfirmContestActionModal" import { CreateContestModal } from "./CreateContestModal" import { ContestScopeModal } from "./ContestScopeModal" import { UpdateContestModal } from "./UpdateContestModal" +import { TelegramBotIndicator } from "./TelegramBotIndicator" type ContestLifeCycleStatus = | "WAITING_INITIAL_PAYMENT" @@ -298,6 +299,7 @@ export const AdminContestsListActive = () => { Starts {DateTime.fromSeconds(c.startDate).toFormat("LLLL d - t")} + diff --git a/src/pages/admin/AdminContestsList/TelegramBotIndicator.tsx b/src/pages/admin/AdminContestsList/TelegramBotIndicator.tsx new file mode 100644 index 00000000..bd01445d --- /dev/null +++ b/src/pages/admin/AdminContestsList/TelegramBotIndicator.tsx @@ -0,0 +1,17 @@ +import { FaTelegram } from "react-icons/fa" +import { Text } from "../../../components/Text" +import { ContestsListItem } from "../../../hooks/api/admin/useAdminContests" + +import styles from "./AdminContestsList.module.scss" + +type Props = { + contest: Pick +} + +export const TelegramBotIndicator: React.FC = ({ contest }) => { + return ( + + {contest.telegramChat ? "Linked to telegram group" : "Telegram bot not added"} + + ) +} From 7ef68164b4033ea5ba442ac68730cb057d7727cd Mon Sep 17 00:00:00 2001 From: Fran Rimoldi Date: Tue, 23 Jan 2024 11:05:06 -0300 Subject: [PATCH 57/80] feat: display tooltip on telegram indicator --- src/components/Tooltip/Tooltip.module.scss | 17 ++++++++ src/components/Tooltip/Tooltip.tsx | 43 +++++++++++++++++++ .../TelegramBotIndicator.tsx | 27 ++++++++++-- 3 files changed, 84 insertions(+), 3 deletions(-) create mode 100644 src/components/Tooltip/Tooltip.module.scss create mode 100644 src/components/Tooltip/Tooltip.tsx diff --git a/src/components/Tooltip/Tooltip.module.scss b/src/components/Tooltip/Tooltip.module.scss new file mode 100644 index 00000000..0a64e846 --- /dev/null +++ b/src/components/Tooltip/Tooltip.module.scss @@ -0,0 +1,17 @@ +@import "../../styles/variables"; + +.container { + display: inline-block; + + &:hover { + cursor: pointer; + } +} + +.tooltip { + position: fixed; + padding: 10px; + + background: $background-color; + border: 1px solid $primary-purple; +} diff --git a/src/components/Tooltip/Tooltip.tsx b/src/components/Tooltip/Tooltip.tsx new file mode 100644 index 00000000..dfb25a09 --- /dev/null +++ b/src/components/Tooltip/Tooltip.tsx @@ -0,0 +1,43 @@ +import React, { PropsWithChildren } from "react" +import cx from "classnames" + +import styles from "./Tooltip.module.scss" + +type Props = { + parent: React.ReactElement + className?: string +} + +export const Tooltip: React.FC> = ({ children, parent, className }) => { + const [isVisible, setIsVisible] = React.useState(false) + const [x, setX] = React.useState(null) + const [y, setY] = React.useState(null) + + const onMouseEnter = (e: React.MouseEvent) => { + setX(e.nativeEvent.clientX) + setY(e.nativeEvent.clientY) + setIsVisible(true) + } + + const onMouseLeave = (e: React.MouseEvent) => { + setIsVisible(false) + } + + return ( +
+ {parent} + {isVisible && ( +
+ {children} +
+ )} +
+ ) +} diff --git a/src/pages/admin/AdminContestsList/TelegramBotIndicator.tsx b/src/pages/admin/AdminContestsList/TelegramBotIndicator.tsx index bd01445d..6a85c082 100644 --- a/src/pages/admin/AdminContestsList/TelegramBotIndicator.tsx +++ b/src/pages/admin/AdminContestsList/TelegramBotIndicator.tsx @@ -1,5 +1,7 @@ import { FaTelegram } from "react-icons/fa" +import { Column } from "../../../components/Layout" import { Text } from "../../../components/Text" +import { Tooltip } from "../../../components/Tooltip/Tooltip" import { ContestsListItem } from "../../../hooks/api/admin/useAdminContests" import styles from "./AdminContestsList.module.scss" @@ -9,9 +11,28 @@ type Props = { } export const TelegramBotIndicator: React.FC = ({ contest }) => { - return ( - - {contest.telegramChat ? "Linked to telegram group" : "Telegram bot not added"} + return contest.telegramChat ? ( + + Linked to telegram group + ) : ( + + Telegram bot not added + + } + > + {contest.telegramChat ? null : ( + + + To link this contest with a Telegram group chat: + + 1. add https://t.me/sherlockdefibot to the protocol's telegram chat + 2. send a link to the dashboard in the chat + After that, it will be automatically linked. + + )} + ) } From 618a490b4a604b3947d9d36d70124902f87d096d Mon Sep 17 00:00:00 2001 From: Rares Stanciu Date: Wed, 14 Feb 2024 11:58:06 +0200 Subject: [PATCH 58/80] feat: generate report for active contests --- src/hooks/api/admin/useAdminContests.ts | 3 + .../AdminContestsListActive.tsx | 89 +++++++++++++++++-- .../AdminContestsListFinished.tsx | 4 +- 3 files changed, 86 insertions(+), 10 deletions(-) diff --git a/src/hooks/api/admin/useAdminContests.ts b/src/hooks/api/admin/useAdminContests.ts index 27d3683e..0089267f 100644 --- a/src/hooks/api/admin/useAdminContests.ts +++ b/src/hooks/api/admin/useAdminContests.ts @@ -33,6 +33,7 @@ export type ContestsListItem = { initialScopeSubmitted: boolean finalScopeSubmitted: boolean telegramChat?: string + finalReportAvailable?: boolean } export type GetAdminContestsResponse = { @@ -63,6 +64,7 @@ export type GetAdminContestsResponse = { initial_scope_submitted: boolean final_scope_submitted: boolean telegram_chat?: string + final_report_available?: boolean } export type ContestListStatus = "active" | "finished" | "draft" @@ -95,6 +97,7 @@ export const parseContest = (d: GetAdminContestsResponse): ContestsListItem => { finalScopeSubmitted: d.final_scope_submitted, nSLOC: d.nsloc, telegramChat: d.telegram_chat, + finalReportAvailable: d.final_report_available, } } diff --git a/src/pages/admin/AdminContestsList/AdminContestsListActive.tsx b/src/pages/admin/AdminContestsList/AdminContestsListActive.tsx index 820b8c75..88965787 100644 --- a/src/pages/admin/AdminContestsList/AdminContestsListActive.tsx +++ b/src/pages/admin/AdminContestsList/AdminContestsListActive.tsx @@ -1,5 +1,5 @@ import { DateTime } from "luxon" -import { useCallback, useState } from "react" +import { useCallback, useEffect, useState } from "react" import { FaClipboardList, FaFastForward, FaPlus, FaEdit, FaRegListAlt } from "react-icons/fa" import { Box } from "../../../components/Box" import { Button } from "../../../components/Button" @@ -16,6 +16,8 @@ import { CreateContestModal } from "./CreateContestModal" import { ContestScopeModal } from "./ContestScopeModal" import { UpdateContestModal } from "./UpdateContestModal" import { TelegramBotIndicator } from "./TelegramBotIndicator" +import { GenerateReportSuccessModal } from "./GenerateReportSuccessModal" +import { useAdminGenerateReport } from "../../../hooks/api/admin/useGenerateReport" type ContestLifeCycleStatus = | "WAITING_INITIAL_PAYMENT" @@ -31,6 +33,8 @@ type ContestLifeCycleStatus = | "SHERLOCK_JUDGING" | "FINISHED" | "DRAFT" + | "FINAL_REPORT_AVAILABLE" + | "FINAL_REPORT_AVAILABLE_TO_GENERATE" const getCurrentStatus = (contest: ContestsListItem): ContestLifeCycleStatus => { if (!contest.initialPayment) return "WAITING_INITIAL_PAYMENT" @@ -39,13 +43,11 @@ const getCurrentStatus = (contest: ContestsListItem): ContestLifeCycleStatus => if (!contest.adminUpcomingApproved) return "READY_TO_PUBLISH" if (!contest.fullPaymentComplete) return "WAITING_ON_FINAL_PAYMENT" if (!contest.adminStartApproved) return "READY_TO_APPROVE_START" + if (contest.status === "CREATED") return "START_APPROVED" + if (contest.auditReport) return "FINAL_REPORT_AVAILABLE" + if (contest.finalReportAvailable) return "FINAL_REPORT_AVAILABLE_TO_GENERATE" - switch (contest.status) { - case "CREATED": - return "START_APPROVED" - default: - return contest.status - } + return contest.status } const getForcedStatus = (contest: ContestsListItem): ContestLifeCycleStatus | undefined => { @@ -79,6 +81,26 @@ export const AdminContestsListActive = () => { const [updateContestIndex, setUpdateContextIndex] = useState() const [scopeModal, setScopeModal] = useState() const [forceActionRowIndex, setForceActionRowIndex] = useState() + const [reportGeneratedModalVisible, setReportGenerateModalVisible] = useState() + const { + generateReport, + isLoading: generateReportIsLoading, + isSuccess, + reset, + variables, + data: reportURL, + } = useAdminGenerateReport() + + useEffect(() => { + if (isSuccess) { + const index = contests?.findIndex((c) => c.id === variables?.contestID) + setReportGenerateModalVisible(index) + console.log("undef") + } else { + setReportGenerateModalVisible(undefined) + console.log("undef") + } + }, [isSuccess, setReportGenerateModalVisible, variables, contests]) const handleActionClick = useCallback( (contestIndex: number, action: ContestAction) => { @@ -113,6 +135,33 @@ export const AdminContestsListActive = () => { }) }, []) + const handleModalClose = useCallback(() => { + console.log("close") + reset() + setReportGenerateModalVisible(undefined) + }, [reset]) + + const handleGenerateReportClick = useCallback( + (index: number) => { + if (!contests) return + + const contest = contests[index] + generateReport({ contestID: contest.id }) + }, + [generateReport, contests] + ) + + const handleViewReportClick = useCallback( + (index: number) => { + if (!contests) return + + setReportGenerateModalVisible(index) + + console.log(contests[index]) + }, + [contests, setReportGenerateModalVisible] + ) + const renderContestAction = useCallback( (contestIndex: number) => { if (!contests) return null @@ -153,13 +202,28 @@ export const AdminContestsListActive = () => { ) + if (status === "FINAL_REPORT_AVAILABLE_TO_GENERATE") + return ( + + ) + + if (status === "FINAL_REPORT_AVAILABLE") + return + return ( ) }, - [contests, handleActionClick, forceActionRowIndex] + [contests, handleActionClick, forceActionRowIndex, handleGenerateReportClick, handleViewReportClick] ) const renderContestState = useCallback( @@ -258,7 +322,7 @@ export const AdminContestsListActive = () => { ) return ( - + @@ -369,6 +433,13 @@ export const AdminContestsListActive = () => { {scopeModal && } + {(reportGeneratedModalVisible === 0 || !!reportGeneratedModalVisible) && contests && ( + + )} ) } diff --git a/src/pages/admin/AdminContestsList/AdminContestsListFinished.tsx b/src/pages/admin/AdminContestsList/AdminContestsListFinished.tsx index 18c22cb3..893ce39f 100644 --- a/src/pages/admin/AdminContestsList/AdminContestsListFinished.tsx +++ b/src/pages/admin/AdminContestsList/AdminContestsListFinished.tsx @@ -128,10 +128,12 @@ export const AdminContestsListFinished = () => { {c.auditReport ? ( - ) : ( + ) : c.finalReportAvailable ? ( + ) : ( + Conditions not met for the final report to be generated )} From c1938e2d34b1c5b8b7c8ce2d93ca689c13877e7d Mon Sep 17 00:00:00 2001 From: Rares Stanciu Date: Thu, 22 Feb 2024 12:02:15 +0200 Subject: [PATCH 59/80] feat: filter and sort draft contests --- package-lock.json | 99 ++++++++++ package.json | 1 + src/components/Tooltip/Tooltip.tsx | 185 +++++++++++++++--- src/hooks/api/admin/useAdminContests.ts | 3 + .../AdminContestListDraft.tsx | 51 ++++- .../AdminContestsList.module.scss | 43 ++++ .../TelegramBotIndicator.tsx | 31 +-- 7 files changed, 363 insertions(+), 50 deletions(-) diff --git a/package-lock.json b/package-lock.json index e2039377..3c70df44 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "sherlock-v2-frontend", "version": "0.1.0", "dependencies": { + "@floating-ui/react": "^0.26.9", "@sentry/react": "^6.19.7", "@sentry/tracing": "^6.19.7", "@testing-library/jest-dom": "^5.16.1", @@ -2951,6 +2952,54 @@ "@ethersproject/strings": "^5.7.0" } }, + "node_modules/@floating-ui/core": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.0.tgz", + "integrity": "sha512-PcF++MykgmTj3CIyOQbKA/hDzOAiqI3mhuoN44WRCopIs1sgoDoU4oty4Jtqaj/y3oDU6fnVSm4QG0a3t5i0+g==", + "dependencies": { + "@floating-ui/utils": "^0.2.1" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.3.tgz", + "integrity": "sha512-RnDthu3mzPlQ31Ss/BTwQ1zjzIhr3lk1gZB1OC56h/1vEtaXkESrOqL5fQVMfXpwGtRwX+YsZBdyHtJMQnkArw==", + "dependencies": { + "@floating-ui/core": "^1.0.0", + "@floating-ui/utils": "^0.2.0" + } + }, + "node_modules/@floating-ui/react": { + "version": "0.26.9", + "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.26.9.tgz", + "integrity": "sha512-p86wynZJVEkEq2BBjY/8p2g3biQ6TlgT4o/3KgFKyTWoJLU1GZ8wpctwRqtkEl2tseYA+kw7dBAIDFcednfI5w==", + "dependencies": { + "@floating-ui/react-dom": "^2.0.8", + "@floating-ui/utils": "^0.2.1", + "tabbable": "^6.0.1" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.0.8.tgz", + "integrity": "sha512-HOdqOt3R3OGeTKidaLvJKcgg75S6tibQ3Tif4eyd91QnIJWr0NLvoXFpJA/j8HqkFSL68GDca9AuyWEHlhyClw==", + "dependencies": { + "@floating-ui/dom": "^1.6.1" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.1.tgz", + "integrity": "sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==" + }, "node_modules/@gar/promisify": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", @@ -19190,6 +19239,11 @@ "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==" }, + "node_modules/tabbable": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", + "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==" + }, "node_modules/tailwindcss": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.1.5.tgz", @@ -22948,6 +23002,46 @@ "@ethersproject/strings": "^5.7.0" } }, + "@floating-ui/core": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.0.tgz", + "integrity": "sha512-PcF++MykgmTj3CIyOQbKA/hDzOAiqI3mhuoN44WRCopIs1sgoDoU4oty4Jtqaj/y3oDU6fnVSm4QG0a3t5i0+g==", + "requires": { + "@floating-ui/utils": "^0.2.1" + } + }, + "@floating-ui/dom": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.3.tgz", + "integrity": "sha512-RnDthu3mzPlQ31Ss/BTwQ1zjzIhr3lk1gZB1OC56h/1vEtaXkESrOqL5fQVMfXpwGtRwX+YsZBdyHtJMQnkArw==", + "requires": { + "@floating-ui/core": "^1.0.0", + "@floating-ui/utils": "^0.2.0" + } + }, + "@floating-ui/react": { + "version": "0.26.9", + "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.26.9.tgz", + "integrity": "sha512-p86wynZJVEkEq2BBjY/8p2g3biQ6TlgT4o/3KgFKyTWoJLU1GZ8wpctwRqtkEl2tseYA+kw7dBAIDFcednfI5w==", + "requires": { + "@floating-ui/react-dom": "^2.0.8", + "@floating-ui/utils": "^0.2.1", + "tabbable": "^6.0.1" + } + }, + "@floating-ui/react-dom": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.0.8.tgz", + "integrity": "sha512-HOdqOt3R3OGeTKidaLvJKcgg75S6tibQ3Tif4eyd91QnIJWr0NLvoXFpJA/j8HqkFSL68GDca9AuyWEHlhyClw==", + "requires": { + "@floating-ui/dom": "^1.6.1" + } + }, + "@floating-ui/utils": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.1.tgz", + "integrity": "sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==" + }, "@gar/promisify": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", @@ -35052,6 +35146,11 @@ "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==" }, + "tabbable": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", + "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==" + }, "tailwindcss": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.1.5.tgz", diff --git a/package.json b/package.json index a692f2a7..aa35eab1 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "private": true, "type": "module", "dependencies": { + "@floating-ui/react": "^0.26.9", "@sentry/react": "^6.19.7", "@sentry/tracing": "^6.19.7", "@testing-library/jest-dom": "^5.16.1", diff --git a/src/components/Tooltip/Tooltip.tsx b/src/components/Tooltip/Tooltip.tsx index dfb25a09..a6d851b7 100644 --- a/src/components/Tooltip/Tooltip.tsx +++ b/src/components/Tooltip/Tooltip.tsx @@ -1,43 +1,168 @@ -import React, { PropsWithChildren } from "react" +import * as React from "react" +import { + useFloating, + autoUpdate, + offset, + flip, + shift, + useHover, + useFocus, + useDismiss, + useRole, + useInteractions, + useMergeRefs, + FloatingPortal, + safePolygon, +} from "@floating-ui/react" +import type { Placement } from "@floating-ui/react" import cx from "classnames" -import styles from "./Tooltip.module.scss" +interface TooltipOptions { + initialOpen?: boolean + placement?: Placement + open?: boolean + onOpenChange?: (open: boolean) => void +} + +export function useTooltip({ + initialOpen = false, + placement = "top", + open: controlledOpen, + onOpenChange: setControlledOpen, +}: TooltipOptions = {}) { + const [uncontrolledOpen, setUncontrolledOpen] = React.useState(initialOpen) + + const open = controlledOpen ?? uncontrolledOpen + const setOpen = setControlledOpen ?? setUncontrolledOpen + + const data = useFloating({ + placement, + open, + onOpenChange: setOpen, + whileElementsMounted: autoUpdate, + middleware: [ + offset(5), + flip({ + crossAxis: placement.includes("-"), + fallbackAxisSideDirection: "start", + padding: 5, + }), + shift({ padding: 5 }), + ], + }) + + const context = data.context -type Props = { - parent: React.ReactElement - className?: string + const hover = useHover(context, { + move: false, + enabled: controlledOpen == null, + handleClose: safePolygon({ requireIntent: false }), + }) + const focus = useFocus(context, { + enabled: controlledOpen == null, + }) + const dismiss = useDismiss(context) + const role = useRole(context, { role: "tooltip" }) + + const interactions = useInteractions([hover, focus, dismiss, role]) + + return React.useMemo( + () => ({ + open, + setOpen, + ...interactions, + ...data, + }), + [open, setOpen, interactions, data] + ) } -export const Tooltip: React.FC> = ({ children, parent, className }) => { - const [isVisible, setIsVisible] = React.useState(false) - const [x, setX] = React.useState(null) - const [y, setY] = React.useState(null) +type ContextType = ReturnType | null + +const TooltipContext = React.createContext(null) + +export const useTooltipContext = () => { + const context = React.useContext(TooltipContext) - const onMouseEnter = (e: React.MouseEvent) => { - setX(e.nativeEvent.clientX) - setY(e.nativeEvent.clientY) - setIsVisible(true) + if (context == null) { + throw new Error("Tooltip components must be wrapped in ") } - const onMouseLeave = (e: React.MouseEvent) => { - setIsVisible(false) + return context +} + +export function Tooltip({ children, ...options }: { children: React.ReactNode } & TooltipOptions) { + // This can accept any props as options, e.g. `placement`, + // or other positioning options. + const tooltip = useTooltip(options) + return {children} +} + +export const TooltipTrigger = React.forwardRef & { asChild?: boolean }>( + function TooltipTrigger({ children, asChild = false, ...props }, propRef) { + const context = useTooltipContext() + const childrenRef = (children as any).ref + const ref = useMergeRefs([context.refs.setReference, propRef, childrenRef]) + + // `asChild` allows the user to pass any element as the anchor + if (asChild && React.isValidElement(children)) { + return React.cloneElement( + children, + context.getReferenceProps({ + ref, + ...props, + ...children.props, + "data-state": context.open ? "open" : "closed", + }) + ) + } + + return ( +
+ {children} +
+ ) } +) + +type TooltipContentProps = { + padding?: boolean +} & React.HTMLProps + +export const TooltipContent = React.forwardRef(function TooltipContent( + { style, padding = true, ...props }, + propRef +) { + const context = useTooltipContext() + const ref = useMergeRefs([context.refs.setFloating, propRef]) + + if (!context.open) return null return ( -
- {parent} - {isVisible && ( -
+
- {children} -
- )} -
+ overflow: "hidden", + borderRadius: 4, + borderWidth: 1, + borderColor: "rgba(84, 10, 152, 0.5)", + borderStyle: "solid", + backgroundColor: "#280745", + padding: padding ? 16 : 0, + }, + }} + {...context.getFloatingProps(props)} + /> + ) -} +}) diff --git a/src/hooks/api/admin/useAdminContests.ts b/src/hooks/api/admin/useAdminContests.ts index 0089267f..1522c5c4 100644 --- a/src/hooks/api/admin/useAdminContests.ts +++ b/src/hooks/api/admin/useAdminContests.ts @@ -31,6 +31,7 @@ export type ContestsListItem = { leadJudgeFixedPay: number fullPayment: number initialScopeSubmitted: boolean + initialScopeSubmittedAt: number | null finalScopeSubmitted: boolean telegramChat?: string finalReportAvailable?: boolean @@ -62,6 +63,7 @@ export type GetAdminContestsResponse = { lead_judge_fixed_pay: number full_payment: number initial_scope_submitted: boolean + initial_scope_submitted_at: number | null final_scope_submitted: boolean telegram_chat?: string final_report_available?: boolean @@ -94,6 +96,7 @@ export const parseContest = (d: GetAdminContestsResponse): ContestsListItem => { leadJudgeFixedPay: d.lead_judge_fixed_pay, fullPayment: d.full_payment, initialScopeSubmitted: d.initial_scope_submitted, + initialScopeSubmittedAt: d.initial_scope_submitted_at, finalScopeSubmitted: d.final_scope_submitted, nSLOC: d.nsloc, telegramChat: d.telegram_chat, diff --git a/src/pages/admin/AdminContestsList/AdminContestListDraft.tsx b/src/pages/admin/AdminContestsList/AdminContestListDraft.tsx index f0b4aaad..b516f5e6 100644 --- a/src/pages/admin/AdminContestsList/AdminContestListDraft.tsx +++ b/src/pages/admin/AdminContestsList/AdminContestListDraft.tsx @@ -1,5 +1,5 @@ -import { useState } from "react" -import { FaClipboardList, FaRecycle, FaRedo, FaTrash, FaUndo } from "react-icons/fa" +import { useMemo, useState } from "react" +import { FaClipboardList, FaFilter, FaRecycle, FaRedo, FaTrash, FaUndo } from "react-icons/fa" import { Box } from "../../../components/Box" import { Button } from "../../../components/Button" import { Column, Row } from "../../../components/Layout" @@ -13,6 +13,8 @@ import styles from "./AdminContestsList.module.scss" import { ConfirmContestModal } from "./ConfirmContestModal" import { ContestResetInitialScopeModal } from "./ContestResetInitialScopeModal" import { DeleteDraftContestModal } from "./DeleteDraftContestModal" +import { Tooltip, TooltipContent, TooltipTrigger } from "../../../components/Tooltip/Tooltip" +import { DateTime } from "luxon" export const AdminContestListDraft = () => { const { data: contests, isLoading } = useAdminContests("draft") @@ -21,11 +23,46 @@ export const AdminContestListDraft = () => { const [resetContestIndex, setResetContestIndex] = useState() const [deleteContestIndex, setDeleteContestIndex] = useState() + const [isFilterActive, setIsFilterActive] = useState(false) + + const visibleContests = useMemo(() => { + if (!isFilterActive) { + return contests + } + + return contests + ?.filter((item) => item.initialScopeSubmitted) + .sort((a, b) => (b.initialScopeSubmittedAt ?? 0) - (a.initialScopeSubmittedAt ?? 0)) + }, [contests, isFilterActive]) + return ( - DRAFTS + + DRAFTS + + +
+ + Filter +
+ {isFilterActive ? "Only with submitted scope" : "All contests"} +
+
+
+ +
+
setIsFilterActive(false)}> + All contests +
+
setIsFilterActive(true)}> + Only with submitted scope +
+
+
+
+
@@ -41,7 +78,7 @@ export const AdminContestListDraft = () => { - {contests?.map((c, index) => { + {visibleContests?.map((c, index) => { return ( @@ -77,7 +114,11 @@ export const AdminContestListDraft = () => {
{c.id} - {c.initialScopeSubmitted ? "Initial scope submitted" : "Waiting on initial scope"} + {c.initialScopeSubmittedAt + ? `Initial scope submitted at ${DateTime.fromSeconds( + c.initialScopeSubmittedAt + ).toLocaleString(DateTime.DATETIME_SHORT)}` + : "Waiting on initial scope"} diff --git a/src/pages/admin/AdminContestsList/AdminContestsList.module.scss b/src/pages/admin/AdminContestsList/AdminContestsList.module.scss index bab23a7f..862e0a50 100644 --- a/src/pages/admin/AdminContestsList/AdminContestsList.module.scss +++ b/src/pages/admin/AdminContestsList/AdminContestsList.module.scss @@ -86,3 +86,46 @@ margin-right: 5px; } } + +.filterContainer { + cursor: pointer; + user-select: none; + display: flex; + flex-direction: row; + align-items: center; + gap: 8px; + transition: all 0.1s linear; + color: rgb(156, 163, 175); + + &:hover { + color: rgb(209, 213, 219); + } + + .activeFilter { + background-color: rgb(209, 213, 219); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + border-radius: 100px; + padding: 4px 8px; + text-align: center; + font-size: 14px; + font-weight: 600; + color: black; + } +} + +.filtersList { + display: flex; + flex-direction: column; + + div { + cursor: pointer; + padding: 16px 32px; + + &:hover { + background-color: rgba(255, 255, 255, 0.1); + } + } +} diff --git a/src/pages/admin/AdminContestsList/TelegramBotIndicator.tsx b/src/pages/admin/AdminContestsList/TelegramBotIndicator.tsx index 6a85c082..04178ef6 100644 --- a/src/pages/admin/AdminContestsList/TelegramBotIndicator.tsx +++ b/src/pages/admin/AdminContestsList/TelegramBotIndicator.tsx @@ -1,7 +1,7 @@ import { FaTelegram } from "react-icons/fa" import { Column } from "../../../components/Layout" import { Text } from "../../../components/Text" -import { Tooltip } from "../../../components/Tooltip/Tooltip" +import { Tooltip, TooltipTrigger, TooltipContent } from "../../../components/Tooltip/Tooltip" import { ContestsListItem } from "../../../hooks/api/admin/useAdminContests" import styles from "./AdminContestsList.module.scss" @@ -16,23 +16,24 @@ export const TelegramBotIndicator: React.FC = ({ contest }) => { Linked to telegram group ) : ( - + Telegram bot not added - } - > - {contest.telegramChat ? null : ( - - - To link this contest with a Telegram group chat: - - 1. add https://t.me/sherlockdefibot to the protocol's telegram chat - 2. send a link to the dashboard in the chat - After that, it will be automatically linked. - - )} + + + {contest.telegramChat ? null : ( + + + To link this contest with a Telegram group chat: + + 1. add https://t.me/sherlockdefibot to the protocol's telegram chat + 2. send a link to the dashboard in the chat + After that, it will be automatically linked. + + )} + ) } From afff4043ce689c48b6be9173310f02105ae00582 Mon Sep 17 00:00:00 2001 From: Fran Rimoldi Date: Tue, 27 Feb 2024 19:08:28 -0300 Subject: [PATCH 60/80] fix: use contest's lead senior selection date --- src/hooks/api/admin/useAdminContests.ts | 3 +++ src/pages/admin/AdminContestsList/AdminContestsListActive.tsx | 4 +--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/hooks/api/admin/useAdminContests.ts b/src/hooks/api/admin/useAdminContests.ts index 1522c5c4..639f66fa 100644 --- a/src/hooks/api/admin/useAdminContests.ts +++ b/src/hooks/api/admin/useAdminContests.ts @@ -22,6 +22,7 @@ export type ContestsListItem = { hasSolidityMetricsReport: boolean leadSeniorAuditorHandle: string leadSeniorSelectionMessageSentAt: number + leadSeniorSelectionDate: number leadSeniorConfirmationMessage: string auditReport?: string nSLOC?: number @@ -54,6 +55,7 @@ export type GetAdminContestsResponse = { has_solidity_metrics_report: boolean lead_senior_auditor_handle: string senior_selection_message_sent_at: number + senior_selection_date: number senior_confirmed_message: string audit_report?: string nsloc?: number @@ -89,6 +91,7 @@ export const parseContest = (d: GetAdminContestsResponse): ContestsListItem => { hasSolidityMetricsReport: d.has_solidity_metrics_report, leadSeniorAuditorHandle: d.lead_senior_auditor_handle, leadSeniorSelectionMessageSentAt: d.senior_selection_message_sent_at, + leadSeniorSelectionDate: d.senior_selection_date, leadSeniorConfirmationMessage: d.senior_confirmed_message, auditReport: d.audit_report, rewards: d.audit_rewards, diff --git a/src/pages/admin/AdminContestsList/AdminContestsListActive.tsx b/src/pages/admin/AdminContestsList/AdminContestsListActive.tsx index 88965787..58c2287f 100644 --- a/src/pages/admin/AdminContestsList/AdminContestsListActive.tsx +++ b/src/pages/admin/AdminContestsList/AdminContestsListActive.tsx @@ -242,9 +242,7 @@ export const AdminContestsListActive = () => { } if (status === "WAITING_FOR_SENIOR_SELECTION") { - const timeLeft = DateTime.fromSeconds(contest.leadSeniorSelectionMessageSentAt) - .plus({ hours: 72 }) - .diffNow(["days", "hours"]) + const timeLeft = DateTime.fromSeconds(contest.leadSeniorSelectionDate).diffNow(["days", "hours"]) return ( Lead Senior Watson selection in progress From 19e02a12ce89324a87346805e285a0f9bc0e6553 Mon Sep 17 00:00:00 2001 From: Rares Stanciu Date: Mon, 4 Mar 2024 11:42:19 +0200 Subject: [PATCH 61/80] feat: draft contests filter --- .../AdminContestListDraft.tsx | 42 ++++++++++++------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/src/pages/admin/AdminContestsList/AdminContestListDraft.tsx b/src/pages/admin/AdminContestsList/AdminContestListDraft.tsx index b516f5e6..a5f76a48 100644 --- a/src/pages/admin/AdminContestsList/AdminContestListDraft.tsx +++ b/src/pages/admin/AdminContestsList/AdminContestListDraft.tsx @@ -1,5 +1,5 @@ import { useMemo, useState } from "react" -import { FaClipboardList, FaFilter, FaRecycle, FaRedo, FaTrash, FaUndo } from "react-icons/fa" +import { FaClipboardList, FaFilter, FaTrash, FaUndo } from "react-icons/fa" import { Box } from "../../../components/Box" import { Button } from "../../../components/Button" import { Column, Row } from "../../../components/Layout" @@ -16,6 +16,14 @@ import { DeleteDraftContestModal } from "./DeleteDraftContestModal" import { Tooltip, TooltipContent, TooltipTrigger } from "../../../components/Tooltip/Tooltip" import { DateTime } from "luxon" +type FilterType = "ALL" | "ONLY_SCOPE_SUBMITTED" | "ONLY_SCOPE_NOT_SUBMITTED" + +const FilterLabels: Record = { + ALL: "All contests", + ONLY_SCOPE_SUBMITTED: "Only with submitted scope", + ONLY_SCOPE_NOT_SUBMITTED: "Only without submitted scope", +} + export const AdminContestListDraft = () => { const { data: contests, isLoading } = useAdminContests("draft") @@ -23,17 +31,20 @@ export const AdminContestListDraft = () => { const [resetContestIndex, setResetContestIndex] = useState() const [deleteContestIndex, setDeleteContestIndex] = useState() - const [isFilterActive, setIsFilterActive] = useState(false) + const [activeFilter, setActiveFilter] = useState("ALL") const visibleContests = useMemo(() => { - if (!isFilterActive) { - return contests + switch (activeFilter) { + case "ALL": + return contests + case "ONLY_SCOPE_SUBMITTED": + return contests + ?.filter((item) => item.initialScopeSubmitted) + .sort((a, b) => (b.initialScopeSubmittedAt ?? 0) - (a.initialScopeSubmittedAt ?? 0)) + case "ONLY_SCOPE_NOT_SUBMITTED": + return contests?.filter((item) => !item.initialScopeSubmitted) } - - return contests - ?.filter((item) => item.initialScopeSubmitted) - .sort((a, b) => (b.initialScopeSubmittedAt ?? 0) - (a.initialScopeSubmittedAt ?? 0)) - }, [contests, isFilterActive]) + }, [contests, activeFilter]) return ( @@ -47,18 +58,17 @@ export const AdminContestListDraft = () => { Filter
- {isFilterActive ? "Only with submitted scope" : "All contests"} + {FilterLabels[activeFilter]}
-
setIsFilterActive(false)}> - All contests -
-
setIsFilterActive(true)}> - Only with submitted scope -
+ {Object.entries(FilterLabels).map(([value, label]) => ( +
setActiveFilter(value as FilterType)}> + {label} +
+ ))}
From 9b6934671eb0309668f096e55b4577c003ecd023 Mon Sep 17 00:00:00 2001 From: Rares Stanciu Date: Wed, 6 Mar 2024 14:27:11 +0200 Subject: [PATCH 62/80] chore: remove github field --- .../AdminContestsList/CreateContestForm.tsx | 27 +++---------------- 1 file changed, 3 insertions(+), 24 deletions(-) diff --git a/src/pages/admin/AdminContestsList/CreateContestForm.tsx b/src/pages/admin/AdminContestsList/CreateContestForm.tsx index 146aaefb..f37ff009 100644 --- a/src/pages/admin/AdminContestsList/CreateContestForm.tsx +++ b/src/pages/admin/AdminContestsList/CreateContestForm.tsx @@ -1,5 +1,5 @@ import { useCallback, useEffect, useMemo, useState } from "react" -import { FaChrome, FaGithub, FaTwitter } from "react-icons/fa" +import { FaChrome, FaTwitter } from "react-icons/fa" import { useDebounce } from "use-debounce" import { BigNumber, ethers } from "ethers" import { DateTime } from "luxon" @@ -22,7 +22,6 @@ export type ContestValues = { id?: number name?: string twitter?: string - githubTeam?: string website?: string logoUrl?: string } @@ -66,7 +65,6 @@ export const CreateContestForm: React.FC = ({ } = useAdminProtocol(debouncedProtocolName) const [protocolTwitter, setProtocolTwitter] = useState(protocol?.twitter ?? "") - const [protocolGithubTeam, setProtocolGithubTeam] = useState(protocol?.githubTeam ?? "") const [protocolWebsite, setProtocolWebsite] = useState(protocol?.website ?? "") const [protocolLogoURL, setProtocolLogoURL] = useState(protocol?.logoURL ?? "") @@ -189,7 +187,6 @@ export const CreateContestForm: React.FC = ({ if (protocolName === "") return false if (protocolLogoURL === "" && !protocol?.logoURL) return false if (protocolWebsite === "" && !protocol?.website) return false - if (protocolGithubTeam === "" && !protocol?.githubTeam) return false } if (contestTitle === "") return false @@ -208,19 +205,18 @@ export const CreateContestForm: React.FC = ({ return true }, [ + draft, + contestShortDescription, contestAuditLength, contestAuditRewards, - contestShortDescription.length, contestStartDate, contestTitle, contestTotalCost, protocol?.logoURL, protocol?.website, - protocol?.githubTeam, protocolLogoURL, protocolName, protocolWebsite, - protocolGithubTeam, contest, ]) @@ -286,7 +282,6 @@ export const CreateContestForm: React.FC = ({ protocol: { id: protocol?.id, name: protocolName === "" ? undefined : protocolName, - githubTeam: protocolGithubTeam === "" ? undefined : protocolGithubTeam, twitter: protocolTwitter === "" ? undefined : protocolTwitter, website: protocolWebsite === "" ? undefined : protocolWebsite, logoUrl: protocolLogoURL === "" ? undefined : protocolLogoURL, @@ -315,7 +310,6 @@ export const CreateContestForm: React.FC = ({ contestTotalCost, onSubmit, protocol?.id, - protocolGithubTeam, protocolLogoURL, protocolName, protocolTwitter, @@ -366,21 +360,6 @@ export const CreateContestForm: React.FC = ({ {displayProtocolInfo && ( <> - - - GitHub - - } - > - - From 4d83eb29604436964ad256bb598283ce6ece9200 Mon Sep 17 00:00:00 2001 From: Rares Stanciu Date: Wed, 13 Mar 2024 14:41:51 +0200 Subject: [PATCH 63/80] feat: best efforts --- src/components/Checkbox/Checkbox.module.scss | 25 +++++++++++++++++++ src/components/Checkbox/Checkbox.tsx | 17 +++++++++++++ src/hooks/api/admin/useAdminConfirmContest.ts | 2 ++ .../AdminContestsList/CreateContestForm.tsx | 16 ++++++++++++ 4 files changed, 60 insertions(+) create mode 100644 src/components/Checkbox/Checkbox.module.scss create mode 100644 src/components/Checkbox/Checkbox.tsx diff --git a/src/components/Checkbox/Checkbox.module.scss b/src/components/Checkbox/Checkbox.module.scss new file mode 100644 index 00000000..9c186b2d --- /dev/null +++ b/src/components/Checkbox/Checkbox.module.scss @@ -0,0 +1,25 @@ +@import "../../styles/variables"; + +.container { + height: 2rem; + width: 2rem; + border-radius: 4px; + border: 1px solid $primary-purple; + color: $primary-purple; + cursor: pointer; + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + transition: all 150ms cubic-bezier(0.4, 0, 0.2, 1); + + &:hover { + border-color: lighten($primary-purple, 20%); + color: lighten($primary-purple, 20%); + } + + &:active { + border-color: darken($primary-purple, 20%); + color: darken($primary-purple, 20%); + } +} diff --git a/src/components/Checkbox/Checkbox.tsx b/src/components/Checkbox/Checkbox.tsx new file mode 100644 index 00000000..61c78666 --- /dev/null +++ b/src/components/Checkbox/Checkbox.tsx @@ -0,0 +1,17 @@ +import { FaCheck } from "react-icons/fa" +import styles from "./Checkbox.module.scss" + +type Props = { + checked?: boolean + onChange?: (checked: boolean) => void +} + +const Checkbox: React.FC = ({ checked = false, onChange }) => { + return ( +
onChange?.(!checked)}> + {checked && } +
+ ) +} + +export default Checkbox diff --git a/src/hooks/api/admin/useAdminConfirmContest.ts b/src/hooks/api/admin/useAdminConfirmContest.ts index a79052a9..7f26262c 100644 --- a/src/hooks/api/admin/useAdminConfirmContest.ts +++ b/src/hooks/api/admin/useAdminConfirmContest.ts @@ -15,6 +15,7 @@ type AdminConfirmContestParams = { judgingPrizePool: number leadJudgeFixedPay: number fullPayment: number + isBestEfforts?: boolean } export const useAdminConfirmContest = () => { @@ -33,6 +34,7 @@ export const useAdminConfirmContest = () => { judging_prize_pool: params.judgingPrizePool, lead_judge_fixed_pay: params.leadJudgeFixedPay, full_payment: params.fullPayment, + is_best_efforts: params.isBestEfforts, }, { diff --git a/src/pages/admin/AdminContestsList/CreateContestForm.tsx b/src/pages/admin/AdminContestsList/CreateContestForm.tsx index f37ff009..933721fe 100644 --- a/src/pages/admin/AdminContestsList/CreateContestForm.tsx +++ b/src/pages/admin/AdminContestsList/CreateContestForm.tsx @@ -16,6 +16,7 @@ import { Text } from "../../../components/Text" import TokenInput from "../../../components/TokenInput/TokenInput" import { Button } from "../../../components/Button" import { ContestsListItem } from "../../../hooks/api/admin/useAdminContests" +import Checkbox from "../../../components/Checkbox/Checkbox" export type ContestValues = { protocol: { @@ -35,6 +36,7 @@ export type ContestValues = { judgingPrizePool: number leadJudgeFixedPay: number fullPayment: number + isBestEfforts?: boolean } } @@ -82,6 +84,7 @@ export const CreateContestForm: React.FC = ({ const [contestJudgingPrizePool, setContestJudgingPrizePool] = useState(BigNumber.from(0)) const [contestLeadJudgeFixedPay, setContestLeadJudgeFixedPay] = useState(BigNumber.from(0)) const [contestTotalCost, setContestTotalCost] = useState(BigNumber.from(0)) + const [contestIsBestEfforts, setContestIsBestEfforts] = useState(false) const [initialTotalRewards, setInitialTotalRewards] = useState(BigNumber.from(0)) const [initialAuditContestRewards, setInitialAuditContestRewards] = useState(BigNumber.from(0)) @@ -296,6 +299,7 @@ export const CreateContestForm: React.FC = ({ judgingPrizePool: parseInt(ethers.utils.formatUnits(contestJudgingPrizePool ?? 0, 6)), leadJudgeFixedPay: parseInt(ethers.utils.formatUnits(contestLeadJudgeFixedPay ?? 0, 6)), fullPayment: parseInt(ethers.utils.formatUnits(contestTotalCost ?? 0, 6)), + isBestEfforts: contestIsBestEfforts, }, }) }, [ @@ -308,6 +312,7 @@ export const CreateContestForm: React.FC = ({ contestStartDate, contestTitle, contestTotalCost, + contestIsBestEfforts, onSubmit, protocol?.id, protocolLogoURL, @@ -477,6 +482,17 @@ export const CreateContestForm: React.FC = ({
{`Admin Fee: ${sherlockFee} USDC`} +
+ + + + Best Efforts Contest + + A Best Efforts contest will not use the regular tiered Lead Senior Watson fixed pay. + + Instead, a fixed 33% of the Audit Contest Rewards will be reserved for the Lead Senior Watson. + + )}
From aabe1e17f8e118190e00c356e52319f33e80ac96 Mon Sep 17 00:00:00 2001 From: Rares Stanciu Date: Mon, 18 Mar 2024 18:59:14 +0200 Subject: [PATCH 64/80] feat: update contest --- .../RadioButton/RadioButton.module.scss | 35 ++ src/components/RadioButton/RadioButton.tsx | 39 ++ src/components/TokenInput/TokenInput.tsx | 22 +- src/hooks/api/admin/useAdminConfirmContest.ts | 12 +- src/hooks/api/admin/useAdminContests.ts | 17 + src/hooks/api/admin/useAdminUpdateContest.ts | 10 + src/pages/Claim/Field.tsx | 6 +- .../CreateContestForm.module.scss | 13 + .../AdminContestsList/CreateContestForm.tsx | 359 ++++++++++++++---- 9 files changed, 429 insertions(+), 84 deletions(-) create mode 100644 src/components/RadioButton/RadioButton.module.scss create mode 100644 src/components/RadioButton/RadioButton.tsx create mode 100644 src/pages/admin/AdminContestsList/CreateContestForm.module.scss diff --git a/src/components/RadioButton/RadioButton.module.scss b/src/components/RadioButton/RadioButton.module.scss new file mode 100644 index 00000000..a69d3802 --- /dev/null +++ b/src/components/RadioButton/RadioButton.module.scss @@ -0,0 +1,35 @@ +@import "../../styles/variables"; + +.options { + border: 3px solid #000; + height: 3.315rem; + box-sizing: border-box; + display: flex; + flex-direction: row; + align-items: center; + + .option { + height: 100%; + width: 100%; + background: darken($primary-purple, 30%); + padding: 0 2rem; + display: flex; + align-items: center; + justify-content: center; + border: 1px solid black; + cursor: pointer; + + &.active { + background: $primary-purple; + font-weight: bold; + } + + &:hover { + background: darken($primary-purple, 10%); + } + + &:active { + background: darken($primary-purple, 35%); + } + } +} diff --git a/src/components/RadioButton/RadioButton.tsx b/src/components/RadioButton/RadioButton.tsx new file mode 100644 index 00000000..6c6f70a0 --- /dev/null +++ b/src/components/RadioButton/RadioButton.tsx @@ -0,0 +1,39 @@ +import { Column, Row } from "../Layout" +import { Text } from "../Text" +import styles from "./RadioButton.module.scss" +import cx from "classnames" + +type Props = { + value: any + label?: string + options: Array<{ + label: string + value: any + }> + onChange: (value: any) => void +} + +const RadioButton: React.FC = ({ options, value, label, onChange }) => { + return ( + + {label && ( + + {label} + + )} +
+ {options.map((option) => ( +
onChange?.(option.value)} + > + {option.label} +
+ ))} +
+
+ ) +} + +export default RadioButton diff --git a/src/components/TokenInput/TokenInput.tsx b/src/components/TokenInput/TokenInput.tsx index 7f9bd57b..82ca71e1 100644 --- a/src/components/TokenInput/TokenInput.tsx +++ b/src/components/TokenInput/TokenInput.tsx @@ -18,6 +18,7 @@ export type Props = Omit, "value" | "onChange"> & { token: InputToken onChange: (value?: BigNumber) => void initialValue?: BigNumber + displayTokenLabel?: boolean } export const decimalsByToken: Record = { @@ -27,7 +28,14 @@ export const decimalsByToken: Record = { const decommify = (value: string) => value.replaceAll(",", "") -export const TokenInput: React.FC = ({ balance, token, onChange, initialValue, ...props }) => { +export const TokenInput: React.FC = ({ + balance, + token, + onChange, + initialValue, + displayTokenLabel = true, + ...props +}) => { const [amount, amountBN, setAmount, setAmountBN] = useAmountState(decimalsByToken[token]) const { disabled } = props @@ -62,11 +70,13 @@ export const TokenInput: React.FC = ({ balance, token, onChange, initialV - - - {token} - - + {displayTokenLabel && ( + + + {token} + + + )} {balance && !disabled && ( diff --git a/src/hooks/api/admin/useAdminConfirmContest.ts b/src/hooks/api/admin/useAdminConfirmContest.ts index 7f26262c..1622a800 100644 --- a/src/hooks/api/admin/useAdminConfirmContest.ts +++ b/src/hooks/api/admin/useAdminConfirmContest.ts @@ -15,7 +15,11 @@ type AdminConfirmContestParams = { judgingPrizePool: number leadJudgeFixedPay: number fullPayment: number - isBestEfforts?: boolean + lswPaymentStructure?: "TIERED" | "BEST_EFFORTS" | "FIXED" + customLswFixedPay?: number | null + private?: boolean + requiresKYC?: boolean + maxNumberOfParticipants?: number | null } export const useAdminConfirmContest = () => { @@ -34,7 +38,11 @@ export const useAdminConfirmContest = () => { judging_prize_pool: params.judgingPrizePool, lead_judge_fixed_pay: params.leadJudgeFixedPay, full_payment: params.fullPayment, - is_best_efforts: params.isBestEfforts, + lsw_payment_structure: params.lswPaymentStructure, + custom_lsw_fixed_pay: params.customLswFixedPay, + private: params.private, + requires_kyc: params.requiresKYC, + max_number_of_participants: params.maxNumberOfParticipants, }, { diff --git a/src/hooks/api/admin/useAdminContests.ts b/src/hooks/api/admin/useAdminContests.ts index 639f66fa..349c2203 100644 --- a/src/hooks/api/admin/useAdminContests.ts +++ b/src/hooks/api/admin/useAdminContests.ts @@ -21,6 +21,7 @@ export type ContestsListItem = { submissionReady: boolean hasSolidityMetricsReport: boolean leadSeniorAuditorHandle: string + leadSeniorAuditorFixedPay: number | null leadSeniorSelectionMessageSentAt: number leadSeniorSelectionDate: number leadSeniorConfirmationMessage: string @@ -36,6 +37,11 @@ export type ContestsListItem = { finalScopeSubmitted: boolean telegramChat?: string finalReportAvailable?: boolean + lswPaymentStructure: "TIERED" | "BEST_EFFORTS" | "FIXED" + customLswFixedPay: number | null + private: boolean + requiresKYC: boolean + maxNumberOfParticipants: number | null } export type GetAdminContestsResponse = { @@ -69,6 +75,11 @@ export type GetAdminContestsResponse = { final_scope_submitted: boolean telegram_chat?: string final_report_available?: boolean + lsw_payment_structure: "TIERED" | "BEST_EFFORTS" | "FIXED" + lead_senior_auditor_fixed_pay: number | null + private: boolean + requires_kyc: boolean + max_number_of_participants: number | null } export type ContestListStatus = "active" | "finished" | "draft" @@ -104,6 +115,12 @@ export const parseContest = (d: GetAdminContestsResponse): ContestsListItem => { nSLOC: d.nsloc, telegramChat: d.telegram_chat, finalReportAvailable: d.final_report_available, + lswPaymentStructure: d.lsw_payment_structure, + customLswFixedPay: d.lead_senior_auditor_fixed_pay, + private: d.private, + requiresKYC: d.requires_kyc, + maxNumberOfParticipants: d.max_number_of_participants, + leadSeniorAuditorFixedPay: d.lead_senior_auditor_fixed_pay, } } diff --git a/src/hooks/api/admin/useAdminUpdateContest.ts b/src/hooks/api/admin/useAdminUpdateContest.ts index 2b5c18af..0d9fc273 100644 --- a/src/hooks/api/admin/useAdminUpdateContest.ts +++ b/src/hooks/api/admin/useAdminUpdateContest.ts @@ -15,6 +15,11 @@ type AdminUpdateContestParams = { judgingPrizePool: number leadJudgeFixedPay: number fullPayment: number + lswPaymentStructure?: "TIERED" | "BEST_EFFORTS" | "FIXED" + customLswFixedPay?: number | null + private?: boolean + requiresKYC?: boolean + maxNumberOfParticipants?: number | null } export const useAdminUpdateContest = () => { @@ -31,6 +36,11 @@ export const useAdminUpdateContest = () => { judging_prize_pool: params.judgingPrizePool, lead_judge_fixed_pay: params.leadJudgeFixedPay, full_payment: params.fullPayment, + lsw_payment_structure: params.lswPaymentStructure, + custom_lsw_fixed_pay: params.customLswFixedPay, + private: params.private, + requires_kyc: params.requiresKYC, + max_number_of_participants: params.maxNumberOfParticipants, }) } catch (error) { const axiosError = error as AxiosError diff --git a/src/pages/Claim/Field.tsx b/src/pages/Claim/Field.tsx index f555bf96..c5971495 100644 --- a/src/pages/Claim/Field.tsx +++ b/src/pages/Claim/Field.tsx @@ -19,7 +19,7 @@ type Props = { export const Field: React.FC> = ({ label, detail, children, sublabel, ...props }) => { return ( - + {(label || props.error) && ( @@ -43,7 +43,9 @@ export const Field: React.FC> = ({ label, detail, child )} - {children} + + {children} + {detail && ( {detail} diff --git a/src/pages/admin/AdminContestsList/CreateContestForm.module.scss b/src/pages/admin/AdminContestsList/CreateContestForm.module.scss new file mode 100644 index 00000000..d2f55a35 --- /dev/null +++ b/src/pages/admin/AdminContestsList/CreateContestForm.module.scss @@ -0,0 +1,13 @@ +@import "../../../styles/variables"; + +.settingsSection { + border: 1px solid darken($primary-purple, 30%); + padding: 2rem; + width: 100%; + // display: flex; + // flex-direction: column; + + input { + width: 100%; + } +} diff --git a/src/pages/admin/AdminContestsList/CreateContestForm.tsx b/src/pages/admin/AdminContestsList/CreateContestForm.tsx index 933721fe..4a474e8c 100644 --- a/src/pages/admin/AdminContestsList/CreateContestForm.tsx +++ b/src/pages/admin/AdminContestsList/CreateContestForm.tsx @@ -16,7 +16,8 @@ import { Text } from "../../../components/Text" import TokenInput from "../../../components/TokenInput/TokenInput" import { Button } from "../../../components/Button" import { ContestsListItem } from "../../../hooks/api/admin/useAdminContests" -import Checkbox from "../../../components/Checkbox/Checkbox" +import RadioButton from "../../../components/RadioButton/RadioButton" +import styles from "./CreateContestForm.module.scss" export type ContestValues = { protocol: { @@ -36,7 +37,11 @@ export type ContestValues = { judgingPrizePool: number leadJudgeFixedPay: number fullPayment: number - isBestEfforts?: boolean + lswPaymentStructure?: "TIERED" | "BEST_EFFORTS" | "FIXED" + customLswFixedPay?: number | null + private?: boolean + requiresKYC?: boolean + maxNumberOfParticipants?: number | null } } @@ -84,13 +89,19 @@ export const CreateContestForm: React.FC = ({ const [contestJudgingPrizePool, setContestJudgingPrizePool] = useState(BigNumber.from(0)) const [contestLeadJudgeFixedPay, setContestLeadJudgeFixedPay] = useState(BigNumber.from(0)) const [contestTotalCost, setContestTotalCost] = useState(BigNumber.from(0)) - const [contestIsBestEfforts, setContestIsBestEfforts] = useState(false) + const [isPrivate, setIsPrivate] = useState(false) + const [requiresKYC, setRequiresKYC] = useState(false) + const [lswPaymentStructure, setLswPaymentStructure] = useState<"TIERED" | "BEST_EFFORTS" | "FIXED">("TIERED") + const [customLswFixedPay, setCustomLswFixedPay] = useState(undefined) + const [hasLimitedContestants, setHasLimitedContestants] = useState(false) + const [maxNumberOfParticipants, setmaxNumberOfParticipants] = useState("") const [initialTotalRewards, setInitialTotalRewards] = useState(BigNumber.from(0)) const [initialAuditContestRewards, setInitialAuditContestRewards] = useState(BigNumber.from(0)) const [initialJudgingPrizePool, setInitialJudgingPrizePool] = useState(BigNumber.from(0)) const [initialLeadJudgeFixedPay, setInitialLeadJudgeFixedPay] = useState(BigNumber.from(0)) const [initialTotalCost, setInitialTotalCost] = useState(BigNumber.from(0)) + const [initialCustomLswFixedPay, setInitialCustomLswFixedPay] = useState(BigNumber.from(0)) const [startDateError, setStartDateError] = useState() const [shortDescriptionError, setShortDescriptionError] = useState() @@ -118,6 +129,13 @@ export const CreateContestForm: React.FC = ({ setInitialJudgingPrizePool(ethers.utils.parseUnits(`${contest.judgingPrizePool}`, 6)) setInitialLeadJudgeFixedPay(ethers.utils.parseUnits(`${contest.leadJudgeFixedPay}`, 6)) setInitialTotalCost(ethers.utils.parseUnits(`${contest.fullPayment}`, 6)) + setIsPrivate(contest.private) + setRequiresKYC(contest.requiresKYC) + setLswPaymentStructure(contest.lswPaymentStructure) + setHasLimitedContestants(!!contest.maxNumberOfParticipants) + setmaxNumberOfParticipants(contest.maxNumberOfParticipants?.toString() ?? "") + setCustomLswFixedPay(ethers.utils.parseUnits(`${contest.leadSeniorAuditorFixedPay ?? 0}`, 6)) + setInitialCustomLswFixedPay(ethers.utils.parseUnits(`${contest.leadSeniorAuditorFixedPay ?? 0}`, 6)) } }, [contest]) @@ -205,6 +223,8 @@ export const CreateContestForm: React.FC = ({ if (startDate < DateTime.now()) return false if (contestAuditRewards?.eq(BigNumber.from(0))) return false if (contestTotalCost?.eq(BigNumber.from(0))) return false + if (lswPaymentStructure === "FIXED" && !customLswFixedPay?.gt(BigNumber.from(0))) return false + if (hasLimitedContestants && (maxNumberOfParticipants === "" || maxNumberOfParticipants === "0")) return false return true }, [ @@ -221,6 +241,10 @@ export const CreateContestForm: React.FC = ({ protocolName, protocolWebsite, contest, + lswPaymentStructure, + customLswFixedPay, + hasLimitedContestants, + maxNumberOfParticipants, ]) useEffect(() => { @@ -299,7 +323,14 @@ export const CreateContestForm: React.FC = ({ judgingPrizePool: parseInt(ethers.utils.formatUnits(contestJudgingPrizePool ?? 0, 6)), leadJudgeFixedPay: parseInt(ethers.utils.formatUnits(contestLeadJudgeFixedPay ?? 0, 6)), fullPayment: parseInt(ethers.utils.formatUnits(contestTotalCost ?? 0, 6)), - isBestEfforts: contestIsBestEfforts, + lswPaymentStructure, + private: isPrivate, + requiresKYC, + customLswFixedPay: customLswFixedPay ? parseInt(ethers.utils.formatUnits(customLswFixedPay ?? 0, 6)) : null, + maxNumberOfParticipants: + hasLimitedContestants && maxNumberOfParticipants && maxNumberOfParticipants !== "" + ? parseInt(maxNumberOfParticipants) + : null, }, }) }, [ @@ -312,13 +343,18 @@ export const CreateContestForm: React.FC = ({ contestStartDate, contestTitle, contestTotalCost, - contestIsBestEfforts, onSubmit, protocol?.id, protocolLogoURL, protocolName, protocolTwitter, protocolWebsite, + lswPaymentStructure, + isPrivate, + requiresKYC, + customLswFixedPay, + maxNumberOfParticipants, + hasLimitedContestants, ]) const isMinimum = useMemo( @@ -354,6 +390,62 @@ export const CreateContestForm: React.FC = ({ [setInitialTotalCost, setInitialAuditContestRewards, setInitialTotalRewards, contestVariables] ) + const lswPaymentStructureDetails = useMemo(() => { + switch (lswPaymentStructure) { + case "TIERED": + return "The Lead Senior Watson fixed pay is determied by his leaderboard ranking." + case "BEST_EFFORTS": + return "The Lead Senior Watson is 33% of the Audit Contest Rewards." + case "FIXED": + return "The Lead Senior Watson is a custom fixed amount." + } + }, [lswPaymentStructure]) + + const handleSetContestSetup = useCallback((contestSetup: "public" | "private" | "1v1" | "custom") => { + if (contestSetup === "public") { + setIsPrivate(false) + setRequiresKYC(false) + setLswPaymentStructure("TIERED") + setHasLimitedContestants(false) + } else if (contestSetup === "private") { + setIsPrivate(true) + setRequiresKYC(true) + setLswPaymentStructure("TIERED") + setHasLimitedContestants(true) + setmaxNumberOfParticipants("10") + } else if (contestSetup === "1v1") { + setIsPrivate(true) + setRequiresKYC(true) + setLswPaymentStructure("FIXED") + setHasLimitedContestants(true) + setmaxNumberOfParticipants("2") + } + }, []) + + const contestSetup = useMemo(() => { + if (!isPrivate && !requiresKYC && lswPaymentStructure === "TIERED" && !hasLimitedContestants) { + return "public" + } else if ( + isPrivate && + requiresKYC && + lswPaymentStructure === "TIERED" && + hasLimitedContestants && + maxNumberOfParticipants === "10" + ) { + return "private" + } else if ( + isPrivate && + requiresKYC && + lswPaymentStructure === "FIXED" && + hasLimitedContestants && + maxNumberOfParticipants === "2" + ) { + return "1v1" + } else { + return "custom" + } + }, [isPrivate, requiresKYC, lswPaymentStructure, hasLimitedContestants, maxNumberOfParticipants]) + return ( {!contest ? ( @@ -423,80 +515,199 @@ export const CreateContestForm: React.FC = ({ {draft ? null : ( - <> + - - - - - - - - - - - - Pricing presets - - {isMinimum ? ( - - Using minimum pricing - - ) : null} - {isRecommended ? ( - - Using recommended pricing - - ) : null} - - - - - - - - - - - - - - - - - - - - - {`Admin Fee: ${sherlockFee} USDC`} -
- - - - Best Efforts Contest - - A Best Efforts contest will not use the regular tiered Lead Senior Watson fixed pay. - - Instead, a fixed 33% of the Audit Contest Rewards will be reserved for the Lead Senior Watson. - - - + + + + + + + + + + + + + + + + + Pricing presets + + {isMinimum ? ( + + Using minimum pricing + + ) : null} + {isRecommended ? ( + + Using recommended pricing + + ) : null} + + + + + + + + + + + + + + + + + + + + + {`Admin Fee: ${sherlockFee} USDC`} + + + +
+ + + + + + {lswPaymentStructureDetails} + + {lswPaymentStructure === "FIXED" && ( + + )} + + + + + {hasLimitedContestants && ( + <> + + + Includes the Lead Senior Watson + + + )} + +
+
+
)}
-
From 352f6364c438e3c887a50c08f789275edfa82b0e Mon Sep 17 00:00:00 2001 From: Fran Rimoldi Date: Wed, 20 Mar 2024 17:16:47 -0300 Subject: [PATCH 65/80] feat: admin/contests redirect --- vercel.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/vercel.json b/vercel.json index def3bd86..a8b4a41b 100644 --- a/vercel.json +++ b/vercel.json @@ -19,6 +19,11 @@ "source": "/admin/payouts", "destination": "https://audits.sherlock.xyz/admin/payouts", "permanent": true + }, + { + "source": "/admin/contests/:path*", + "destination": "https://audits.sherlock.xyz/admin/contests/:path*", + "permanent": true } ] } From 509ce83a1b0b3b2f8023d503ceee8c35636366ba Mon Sep 17 00:00:00 2001 From: Fran Rimoldi Date: Wed, 20 Mar 2024 22:19:32 -0300 Subject: [PATCH 66/80] admin contests redirect --- vercel.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vercel.json b/vercel.json index a8b4a41b..baec9b85 100644 --- a/vercel.json +++ b/vercel.json @@ -21,8 +21,8 @@ "permanent": true }, { - "source": "/admin/contests/:path*", - "destination": "https://audits.sherlock.xyz/admin/contests/:path*", + "source": "/admin/contests/([^/]+)/(.+)", + "destination": "https://audits.sherlock.xyz/admin/contests/$1/$2", "permanent": true } ] From c1392091adc48941ad06037e59afca0b98ab7cea Mon Sep 17 00:00:00 2001 From: Fran Rimoldi Date: Thu, 21 Mar 2024 00:55:54 -0300 Subject: [PATCH 67/80] feat: add button to draft contests --- .../AdminContestsList/AdminContestListDraft.tsx | 12 +++++++++++- .../AdminContestsList/AdminContestsListActive.tsx | 2 ++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/pages/admin/AdminContestsList/AdminContestListDraft.tsx b/src/pages/admin/AdminContestsList/AdminContestListDraft.tsx index a5f76a48..db86da53 100644 --- a/src/pages/admin/AdminContestsList/AdminContestListDraft.tsx +++ b/src/pages/admin/AdminContestsList/AdminContestListDraft.tsx @@ -1,5 +1,5 @@ import { useMemo, useState } from "react" -import { FaClipboardList, FaFilter, FaTrash, FaUndo } from "react-icons/fa" +import { FaClipboardList, FaFilter, FaTrash, FaUndo, FaUsers } from "react-icons/fa" import { Box } from "../../../components/Box" import { Button } from "../../../components/Button" import { Column, Row } from "../../../components/Layout" @@ -15,6 +15,7 @@ import { ContestResetInitialScopeModal } from "./ContestResetInitialScopeModal" import { DeleteDraftContestModal } from "./DeleteDraftContestModal" import { Tooltip, TooltipContent, TooltipTrigger } from "../../../components/Tooltip/Tooltip" import { DateTime } from "luxon" +import { useNavigate } from "react-router-dom" type FilterType = "ALL" | "ONLY_SCOPE_SUBMITTED" | "ONLY_SCOPE_NOT_SUBMITTED" @@ -33,6 +34,8 @@ export const AdminContestListDraft = () => { const [activeFilter, setActiveFilter] = useState("ALL") + const navigate = useNavigate() + const visibleContests = useMemo(() => { switch (activeFilter) { case "ALL": @@ -108,6 +111,13 @@ export const AdminContestListDraft = () => { > + diff --git a/src/pages/admin/AdminContestsList/AdminContestsList.module.scss b/src/pages/admin/AdminContestsList/AdminContestsList.module.scss index 862e0a50..796e049d 100644 --- a/src/pages/admin/AdminContestsList/AdminContestsList.module.scss +++ b/src/pages/admin/AdminContestsList/AdminContestsList.module.scss @@ -24,10 +24,10 @@ width: 30%; } &:nth-child(3) { - width: 20%; + width: 25%; } &:nth-child(4) { - width: 45%; + width: 40%; } &:nth-child(5) { width: 12rem; diff --git a/src/pages/admin/AdminContestsList/AdminContestsListActive.tsx b/src/pages/admin/AdminContestsList/AdminContestsListActive.tsx index 8b25ee54..9a81840e 100644 --- a/src/pages/admin/AdminContestsList/AdminContestsListActive.tsx +++ b/src/pages/admin/AdminContestsList/AdminContestsListActive.tsx @@ -1,6 +1,6 @@ import { DateTime } from "luxon" import { useCallback, useEffect, useState } from "react" -import { FaClipboardList, FaFastForward, FaPlus, FaEdit, FaRegListAlt } from "react-icons/fa" +import { FaClipboardList, FaFastForward, FaPlus, FaEdit, FaRegListAlt, FaUsers } from "react-icons/fa" import { Box } from "../../../components/Box" import { Button } from "../../../components/Button" import { Column, Row } from "../../../components/Layout" @@ -18,7 +18,6 @@ import { UpdateContestModal } from "./UpdateContestModal" import { TelegramBotIndicator } from "./TelegramBotIndicator" import { GenerateReportSuccessModal } from "./GenerateReportSuccessModal" import { useAdminGenerateReport } from "../../../hooks/api/admin/useGenerateReport" -import { useNavigate } from "react-router-dom" type ContestLifeCycleStatus = | "WAITING_INITIAL_PAYMENT" @@ -91,7 +90,6 @@ export const AdminContestsListActive = () => { variables, data: reportURL, } = useAdminGenerateReport() - const navigate = useNavigate() useEffect(() => { if (isSuccess) { @@ -377,6 +375,13 @@ export const AdminContestsListActive = () => { > + +
+ + {createContestModalOpen && setCreateContestModalOpen(false)} />}
) } diff --git a/src/pages/admin/AdminContestsList/AdminContestsListActive.tsx b/src/pages/admin/AdminContestsList/AdminContestsListActive.tsx index a3ac6390..0f987477 100644 --- a/src/pages/admin/AdminContestsList/AdminContestsListActive.tsx +++ b/src/pages/admin/AdminContestsList/AdminContestsListActive.tsx @@ -12,7 +12,6 @@ import { ContestsListItem, useAdminContests } from "../../../hooks/api/admin/use import styles from "./AdminContestsList.module.scss" import { ConfirmContestActionModal } from "./ConfirmContestActionModal" -import { CreateContestModal } from "./CreateContestModal" import { ContestScopeModal } from "./ContestScopeModal" import { UpdateContestModal } from "./UpdateContestModal" import { TelegramBotIndicator } from "./TelegramBotIndicator" @@ -77,7 +76,6 @@ type ConfirmationModal = { export const AdminContestsListActive = () => { const { data: contests, isLoading } = useAdminContests("active") const [confirmationModal, setConfirmationModal] = useState() - const [createContestModalOpen, setCreateContestModalOpen] = useState(false) const [updateContestIndex, setUpdateContextIndex] = useState() const [scopeModal, setScopeModal] = useState() const [forceActionRowIndex, setForceActionRowIndex] = useState() @@ -322,14 +320,6 @@ export const AdminContestsListActive = () => { return ( - - - - - CONTESTS @@ -429,7 +419,6 @@ export const AdminContestsListActive = () => { contest={contests[updateContestIndex]} /> )} - {createContestModalOpen && setCreateContestModalOpen(false)} />} {scopeModal && } From 448893f8ef572442ab2a0763979bd3babcd26c32 Mon Sep 17 00:00:00 2001 From: Rares Stanciu Date: Tue, 23 Apr 2024 01:19:12 +0300 Subject: [PATCH 73/80] fix: freeze --- src/pages/FundraisingClaim/FundraisingClaim.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/FundraisingClaim/FundraisingClaim.tsx b/src/pages/FundraisingClaim/FundraisingClaim.tsx index dd3a76b3..d70697f3 100644 --- a/src/pages/FundraisingClaim/FundraisingClaim.tsx +++ b/src/pages/FundraisingClaim/FundraisingClaim.tsx @@ -71,7 +71,7 @@ export const FundraisingClaimPage = () => { } fetchClaimIsActive() - }) + }, [sherClaim]) const handleOnSuccess = useCallback( async (blockNumber: number) => { From 7bb8c7ab88837728205455f25fb0040d05826b35 Mon Sep 17 00:00:00 2001 From: Fran Rimoldi Date: Wed, 8 May 2024 09:26:04 -0300 Subject: [PATCH 74/80] fix: add auto restake warning --- .../StakingPositionsList.tsx | 29 ++++++------------- .../FundraisingClaim/FundraisingClaim.tsx | 2 +- 2 files changed, 10 insertions(+), 21 deletions(-) diff --git a/src/components/StakingPositionsList/StakingPositionsList.tsx b/src/components/StakingPositionsList/StakingPositionsList.tsx index 3b220fef..3a5c2440 100644 --- a/src/components/StakingPositionsList/StakingPositionsList.tsx +++ b/src/components/StakingPositionsList/StakingPositionsList.tsx @@ -136,30 +136,19 @@ export const StakingPositionsList: React.FC = () => { navigate("/") }, [navigate]) - const mapleAlertVisible = useMemo(() => positions?.some((item) => item.id <= 442), [positions]) - if (!data) return null return ( - {mapleAlertVisible && ( - - - Stakers affected by the Maple loss - - A portion of the Maple funds will be airdropped directly to staker addresses instead of delivered when - unstaking. More info{" "} - - here - - . - - - - )} + + + Important Information for Unstaking + + If a position is not unstaked within two weeks of unlocking, the position will get automatically restaked + for 6 months + + +
{positions.map((position) => ( { - Claiming {fundraisePositionData?.claimableAt < DateTime.now() ? "started" : "starts in"} + Claiming {fundraisePositionData?.claimableAt < DateTime.now() ? " started" : " starts in"} From 0d650cfd6904512c2a5dede81fc36c30af1d285e Mon Sep 17 00:00:00 2001 From: Rares Stanciu Date: Thu, 1 Aug 2024 17:31:16 +0300 Subject: [PATCH 75/80] feat: show excess coverage --- src/hooks/api/stats.ts | 17 +++++++++ src/hooks/api/urls.ts | 1 + src/pages/Overview/ExcessCoverageChart.tsx | 10 ++--- src/pages/Overview/Overview.tsx | 44 ++++++++-------------- 4 files changed, 39 insertions(+), 33 deletions(-) diff --git a/src/hooks/api/stats.ts b/src/hooks/api/stats.ts index 142471be..ea63cb7a 100644 --- a/src/hooks/api/stats.ts +++ b/src/hooks/api/stats.ts @@ -5,6 +5,7 @@ import { getAPYOverTime as getAPYOverTimeUrl, getTVCOverTime as getTVCOverTimeUrl, getTVLOverTime as getTVLOverTimeUrl, + getExternalCoverageOverTime as getExternalCoverageOverTimeUrl, } from "./urls" type APYDataPoint = { @@ -81,6 +82,22 @@ export const useTVCOverTime = () => .sort((a, b) => a.timestamp - b.timestamp) }) +export const externalCoverageOverTimeQueryKey = "externalCoverageOverTime" +export const useExternalCoverageOverTime = () => + useQuery[] | null, Error>(externalCoverageOverTimeQueryKey, async () => { + const { data: response } = await axios.get(getExternalCoverageOverTimeUrl()) + + if (response.ok === false) throw Error(response.error) + if (response.data === null) return null + + return response.data + .map((r) => ({ + timestamp: r.timestamp, + value: BigNumber.from(r.value), + })) + .sort((a, b) => a.timestamp - b.timestamp) + }) + export const tvlOverTimeQueryKey = "tvlOverTime" export const useTVLOverTime = () => useQuery[] | null, Error>(tvlOverTimeQueryKey, async () => { diff --git a/src/hooks/api/urls.ts b/src/hooks/api/urls.ts index e58d0d24..60c0935a 100644 --- a/src/hooks/api/urls.ts +++ b/src/hooks/api/urls.ts @@ -5,6 +5,7 @@ export const getUnlockOverTime = () => "stats/unlock" export const getAPYOverTime = () => "stats/apy" export const getTVLOverTime = () => "stats_tvl" export const getTVCOverTime = () => "stats_tvc" +export const getExternalCoverageOverTime = () => "stats_external_coverage" export const getStakePositions = (account?: string) => (account ? `staking/${account}` : "staking") export const getCoveredProtocols = () => "protocols" export const getLastIndexedBlock = () => `last-block-indexed` diff --git a/src/pages/Overview/ExcessCoverageChart.tsx b/src/pages/Overview/ExcessCoverageChart.tsx index 1b670570..9d16a39d 100644 --- a/src/pages/Overview/ExcessCoverageChart.tsx +++ b/src/pages/Overview/ExcessCoverageChart.tsx @@ -1,6 +1,6 @@ import { Chart } from "../../components/Chart/Chart" import { DateTime } from "luxon" -import { useTVCOverTime } from "../../hooks/api/stats" +import { useExternalCoverageOverTime } from "../../hooks/api/stats" import { useMemo } from "react" import { utils } from "ethers" @@ -17,11 +17,11 @@ const tooltipTitles: Record = { const nexusStartDate = DateTime.fromSeconds(config.nexusMutualStartTimestamp) export const ExcessCoverageChart = () => { - const { data: tvcData } = useTVCOverTime() + const { data: externalCoverageData } = useExternalCoverageOverTime() const chartData = useMemo( () => - tvcData?.reduce<{ name: number; value: number }[]>((dataPoints, item) => { + externalCoverageData?.reduce<{ name: number; value: number }[]>((dataPoints, item) => { const date = DateTime.fromSeconds(item.timestamp) if (nexusStartDate.diff(date, "days").days < 5) { @@ -31,13 +31,13 @@ export const ExcessCoverageChart = () => { dataPoints.push({ name: item.timestamp, - value: date > nexusStartDate ? Number(utils.formatUnits(item.value.mul(25).div(100), 6)) : 0, + value: Number(utils.formatUnits(item.value, 6)), }) } return dataPoints }, []), - [tvcData] + [externalCoverageData] ) const totalAmount = useMemo(() => { diff --git a/src/pages/Overview/Overview.tsx b/src/pages/Overview/Overview.tsx index 661606f6..76dab00c 100644 --- a/src/pages/Overview/Overview.tsx +++ b/src/pages/Overview/Overview.tsx @@ -6,16 +6,14 @@ import { Column, Row } from "../../components/Layout" import { Title } from "../../components/Title" import { Chart } from "../../components/Chart/Chart" -import { useTVCOverTime, useTVLOverTime } from "../../hooks/api/stats" +import { useTVCOverTime, useTVLOverTime, useExternalCoverageOverTime } from "../../hooks/api/stats" import styles from "./Overview.module.scss" import APYChart from "../../components/APYChart/APYChart" import CoveredProtocolsList from "../../components/CoveredProtocolsList/CoveredProtocolsList" import { formatAmount } from "../../utils/format" import StrategiesList from "../../components/StrategiesList/StrategiesList" -import config from "../../config" import { ExcessCoverageChart } from "./ExcessCoverageChart" -import { Text } from "../../components/Text" import ClaimsList from "../../components/ClaimsList/ClaimsList" type ChartDataPoint = { @@ -26,33 +24,19 @@ type ChartDataPoint = { export const OverviewPage: React.FC = () => { const { data: tvlData } = useTVLOverTime() const { data: tvcData } = useTVCOverTime() + const { data: externalCoverageData } = useExternalCoverageOverTime() const chartsData = useMemo(() => { - if (!tvlData || !tvcData) return + if (!tvlData || !tvcData || !externalCoverageData) return const tvcChartData: ChartDataPoint[] = [] const tvlChartData: ChartDataPoint[] = [] const capitalEfficiencyChartData: ChartDataPoint[] = [] - for (let i = 0, j = 0; i < tvlData.length && j < tvcData.length; ) { - const tvcDataPointDate = DateTime.fromSeconds(tvcData[i].timestamp) - const tvlDataPointDate = DateTime.fromSeconds(tvlData[j].timestamp) - - let tvc = tvcData[i] - let tvl = tvlData[j] - - if (tvcDataPointDate.day !== tvlDataPointDate.day) { - if (tvcDataPointDate < tvlDataPointDate) { - tvl = j > 0 ? tvlData[j - 1] : tvlData[j] - i++ - } else { - tvc = i > 0 ? tvcData[i - 1] : tvcData[i] - j++ - } - } else { - i++ - j++ - } + for (let i = 0; i < tvcData.length; i++) { + const tvc = tvcData[i] + const tvl = tvlData[i] + const externalCoverage = externalCoverageData[i] const timestamp = Math.min(tvc.timestamp, tvl.timestamp) @@ -66,9 +50,10 @@ export const OverviewPage: React.FC = () => { value: Number(utils.formatUnits(tvl.value, 6)), } - // TVC is increased by 25% due to our agreement with Nexus. - // To calculate capital efficiency, we only used what is being covered by Sherlock's staking pool. - const sherlockTVC = timestamp > config.nexusMutualStartTimestamp ? tvc.value.mul(75).div(100) : tvc.value + // External coverage is subtracted from the total TVC due to our agreement with Nexus + // and only a part of the TVC is covereged by Sherlock's staking pool. + // const sherlockTVC = timestamp > config.nexusMutualStartTimestamp ? tvc.value.mul(75).div(100) : tvc.value + const sherlockTVC = tvc.value.sub(externalCoverage.value) const capitalEfficiencyDataPoint = { name: timestamp, @@ -83,7 +68,10 @@ export const OverviewPage: React.FC = () => { tvcChartData.push(tvcDataPoint) tvlChartData.push(tvlDataPoint) - capitalEfficiencyChartData.push(capitalEfficiencyDataPoint) + + if (capitalEfficiencyDataPoint.value > 0) { + capitalEfficiencyChartData.push(capitalEfficiencyDataPoint) + } } return { @@ -91,7 +79,7 @@ export const OverviewPage: React.FC = () => { tvlChartData, capitalEfficiencyChartData, } - }, [tvlData, tvcData]) + }, [tvlData, tvcData, externalCoverageData]) return ( From d9ac590be2fb0862a3b0956371b25dd5d9b0a514 Mon Sep 17 00:00:00 2001 From: Fran Rimoldi Date: Tue, 10 Sep 2024 11:54:21 -0300 Subject: [PATCH 76/80] feat: update admin pricing formula --- src/hooks/api/admin/useAdminPricing.ts | 64 +++++++++++++++++++ .../AdminContestsList/CreateContestForm.tsx | 17 +++-- 2 files changed, 74 insertions(+), 7 deletions(-) create mode 100644 src/hooks/api/admin/useAdminPricing.ts diff --git a/src/hooks/api/admin/useAdminPricing.ts b/src/hooks/api/admin/useAdminPricing.ts new file mode 100644 index 00000000..b95eb9a8 --- /dev/null +++ b/src/hooks/api/admin/useAdminPricing.ts @@ -0,0 +1,64 @@ +import { useQuery } from "react-query" +import { contests as contestsAPI } from "../axios" + +type PricingType = "MINIMUM" | "RECOMMENDED" + +type GetAdminPricingResponse = { + audit_contest_rewards: number + bottom_3rd_contest_pot: number + bottom_3rd_lsw_pay: number + judging_prize_pool: number + lead_judge_fixed_pay: number + length: number + middle_3rd_contest_pot: number + middle_3rd_lsw_pay: number + referral_fee: number + reserved_auditor_fixed_pay: number | null + sherlock_fee: number + top_3rd_contest_pot: number + top_3rd_lsw_pay: number + total_price: number + total_rewards: number + type: PricingType +}[] + +type Pricing = { + minTotalRewards: number + minContestRewards: number + minTotalPrice: number + minLeadJudgeFixedPay: number + minJudgingPrizePool: number + recTotalRewards: number + recContestRewards: number + recTotalPrice: number + recLeadJudgeFixedPay: number + recJudgingPrizePool: number + length: number +} + +export const adminPricingQueryKey = (nSLOC: number) => ["contest-variables", nSLOC] +export const useAdminPricing = (nSLOC: number) => + useQuery(adminPricingQueryKey(nSLOC), async (): Promise => { + const { data } = await contestsAPI.get(`/internal/simulate-pricing?nsloc=${nSLOC}`) + + const minimumPricing = data.find((p) => p.type === "MINIMUM") + const recommendedPricing = data.find((p) => p.type === "RECOMMENDED") + + if (!minimumPricing || !recommendedPricing) { + throw Error("Missing pricing data") + } + + return { + minTotalRewards: minimumPricing.total_rewards, + minContestRewards: minimumPricing.audit_contest_rewards, + minTotalPrice: minimumPricing.total_price, + minLeadJudgeFixedPay: minimumPricing.lead_judge_fixed_pay, + minJudgingPrizePool: minimumPricing.judging_prize_pool, + recTotalRewards: recommendedPricing.total_rewards, + recContestRewards: recommendedPricing.audit_contest_rewards, + recTotalPrice: recommendedPricing.total_price, + recLeadJudgeFixedPay: recommendedPricing.lead_judge_fixed_pay, + recJudgingPrizePool: recommendedPricing?.judging_prize_pool, + length: data.length, + } + }) diff --git a/src/pages/admin/AdminContestsList/CreateContestForm.tsx b/src/pages/admin/AdminContestsList/CreateContestForm.tsx index 77977a65..b877189a 100644 --- a/src/pages/admin/AdminContestsList/CreateContestForm.tsx +++ b/src/pages/admin/AdminContestsList/CreateContestForm.tsx @@ -20,6 +20,7 @@ import RadioButton from "../../../components/RadioButton/RadioButton" import styles from "./CreateContestForm.module.scss" import { useAdminProtocolContests } from "../../../hooks/api/admin/useAdminProtocolContests" import Select from "../../../components/Select/Select" +import { useAdminPricing } from "../../../hooks/api/admin/useAdminPricing" export type ContestValues = { protocol: { @@ -135,9 +136,7 @@ export const CreateContestForm: React.FC = ({ const displayProtocolInfo = !!protocol || protocolNotFound || protocolLoading - const { data: contestVariables, isSuccess: contestVariablesSuccess } = useAdminContestVariables( - parseInt(debouncedContestNSLOC) - ) + const { data: contestVariables, isSuccess: adminPricingSuccess } = useAdminPricing(parseInt(debouncedContestNSLOC)) useEffect(() => { if (contest) { @@ -167,15 +166,15 @@ export const CreateContestForm: React.FC = ({ }, [contest]) useEffect(() => { - if (contestVariablesSuccess && (!contest || contest.status === "DRAFT")) { + if (adminPricingSuccess && (!contest || contest.status === "DRAFT")) { setContestAuditLength(`${contestVariables.length}`) setInitialTotalRewards(ethers.utils.parseUnits(`${contestVariables.minTotalRewards}`, 6)) setInitialAuditContestRewards(ethers.utils.parseUnits(`${contestVariables.minContestRewards}`, 6)) - setInitialJudgingPrizePool(ethers.utils.parseUnits(`${contestVariables.judgingPrizePool}`, 6)) - setInitialLeadJudgeFixedPay(ethers.utils.parseUnits(`${contestVariables.leadJudgeFixedPay}`, 6)) + setInitialJudgingPrizePool(ethers.utils.parseUnits(`${contestVariables.minJudgingPrizePool}`, 6)) + setInitialLeadJudgeFixedPay(ethers.utils.parseUnits(`${contestVariables.minLeadJudgeFixedPay}`, 6)) setInitialTotalCost(ethers.utils.parseUnits(`${contestVariables.minTotalPrice}`, 6)) } - }, [contestVariablesSuccess, setContestAuditLength, contestVariables, contest]) + }, [adminPricingSuccess, setContestAuditLength, contestVariables, contest]) useEffect(() => { const diff = contestTotalRewards @@ -411,10 +410,14 @@ export const CreateContestForm: React.FC = ({ setInitialTotalRewards(ethers.utils.parseUnits(`${contestVariables?.minTotalRewards}`, 6)) setInitialAuditContestRewards(ethers.utils.parseUnits(`${contestVariables?.minContestRewards}`, 6)) setInitialTotalCost(ethers.utils.parseUnits(`${contestVariables?.minTotalPrice}`, 6)) + setInitialLeadJudgeFixedPay(ethers.utils.parseUnits(`${contestVariables?.minLeadJudgeFixedPay}`, 6)) + setInitialJudgingPrizePool(ethers.utils.parseUnits(`${contestVariables?.minJudgingPrizePool}`, 6)) } else { setInitialTotalRewards(ethers.utils.parseUnits(`${contestVariables?.recTotalRewards}`, 6)) setInitialAuditContestRewards(ethers.utils.parseUnits(`${contestVariables?.recContestRewards}`, 6)) setInitialTotalCost(ethers.utils.parseUnits(`${contestVariables?.recTotalPrice}`, 6)) + setInitialLeadJudgeFixedPay(ethers.utils.parseUnits(`${contestVariables?.recLeadJudgeFixedPay}`, 6)) + setInitialJudgingPrizePool(ethers.utils.parseUnits(`${contestVariables?.recJudgingPrizePool}`, 6)) } }, [setInitialTotalCost, setInitialAuditContestRewards, setInitialTotalRewards, contestVariables] From c948d15436b2a36c2b64d42d45b546f387b147d6 Mon Sep 17 00:00:00 2001 From: Fran Rimoldi Date: Tue, 24 Sep 2024 08:34:24 -0300 Subject: [PATCH 77/80] fix: pricing formula length --- src/hooks/api/admin/useAdminPricing.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooks/api/admin/useAdminPricing.ts b/src/hooks/api/admin/useAdminPricing.ts index b95eb9a8..3da84ffe 100644 --- a/src/hooks/api/admin/useAdminPricing.ts +++ b/src/hooks/api/admin/useAdminPricing.ts @@ -59,6 +59,6 @@ export const useAdminPricing = (nSLOC: number) => recTotalPrice: recommendedPricing.total_price, recLeadJudgeFixedPay: recommendedPricing.lead_judge_fixed_pay, recJudgingPrizePool: recommendedPricing?.judging_prize_pool, - length: data.length, + length: minimumPricing.length, } }) From cd6f9cd0c17b30d29c3b644721113c4f4a914394 Mon Sep 17 00:00:00 2001 From: Fran Rimoldi Date: Thu, 24 Oct 2024 21:53:47 -0300 Subject: [PATCH 78/80] feat: add custom token to contest form --- src/hooks/api/admin/useAdminContests.ts | 3 ++ src/hooks/api/admin/useAdminPricing.ts | 10 +++--- .../AdminContestsList/CreateContestForm.tsx | 31 ++++++++++++++----- 3 files changed, 32 insertions(+), 12 deletions(-) diff --git a/src/hooks/api/admin/useAdminContests.ts b/src/hooks/api/admin/useAdminContests.ts index 349c2203..909de0c6 100644 --- a/src/hooks/api/admin/useAdminContests.ts +++ b/src/hooks/api/admin/useAdminContests.ts @@ -42,6 +42,7 @@ export type ContestsListItem = { private: boolean requiresKYC: boolean maxNumberOfParticipants: number | null + token: string } export type GetAdminContestsResponse = { @@ -80,6 +81,7 @@ export type GetAdminContestsResponse = { private: boolean requires_kyc: boolean max_number_of_participants: number | null + token: string } export type ContestListStatus = "active" | "finished" | "draft" @@ -121,6 +123,7 @@ export const parseContest = (d: GetAdminContestsResponse): ContestsListItem => { requiresKYC: d.requires_kyc, maxNumberOfParticipants: d.max_number_of_participants, leadSeniorAuditorFixedPay: d.lead_senior_auditor_fixed_pay, + token: d.token, } } diff --git a/src/hooks/api/admin/useAdminPricing.ts b/src/hooks/api/admin/useAdminPricing.ts index 3da84ffe..3348ee2d 100644 --- a/src/hooks/api/admin/useAdminPricing.ts +++ b/src/hooks/api/admin/useAdminPricing.ts @@ -36,10 +36,12 @@ type Pricing = { length: number } -export const adminPricingQueryKey = (nSLOC: number) => ["contest-variables", nSLOC] -export const useAdminPricing = (nSLOC: number) => - useQuery(adminPricingQueryKey(nSLOC), async (): Promise => { - const { data } = await contestsAPI.get(`/internal/simulate-pricing?nsloc=${nSLOC}`) +export const adminPricingQueryKey = (nSLOC: number, token: string) => ["contest-variables", nSLOC, token] +export const useAdminPricing = (nSLOC: number, token: string) => + useQuery(adminPricingQueryKey(nSLOC, token), async (): Promise => { + const { data } = await contestsAPI.get( + `/internal/simulate-pricing?nsloc=${nSLOC}&token=${token}` + ) const minimumPricing = data.find((p) => p.type === "MINIMUM") const recommendedPricing = data.find((p) => p.type === "RECOMMENDED") diff --git a/src/pages/admin/AdminContestsList/CreateContestForm.tsx b/src/pages/admin/AdminContestsList/CreateContestForm.tsx index b877189a..ecf20be8 100644 --- a/src/pages/admin/AdminContestsList/CreateContestForm.tsx +++ b/src/pages/admin/AdminContestsList/CreateContestForm.tsx @@ -136,7 +136,12 @@ export const CreateContestForm: React.FC = ({ const displayProtocolInfo = !!protocol || protocolNotFound || protocolLoading - const { data: contestVariables, isSuccess: adminPricingSuccess } = useAdminPricing(parseInt(debouncedContestNSLOC)) + const [token, setToken] = useState(contest?.token ?? "USDC") + + const { data: contestVariables, isSuccess: adminPricingSuccess } = useAdminPricing( + parseInt(debouncedContestNSLOC), + token + ) useEffect(() => { if (contest) { @@ -604,6 +609,16 @@ export const CreateContestForm: React.FC = ({ + Pricing presets @@ -631,7 +646,7 @@ export const CreateContestForm: React.FC = ({ token="USDC" initialValue={initialTotalRewards} onChange={setContestTotalRewards} - placeholder="USDC" + placeholder={token} persistPlaceholder displayTokenLabel={false} /> @@ -641,7 +656,7 @@ export const CreateContestForm: React.FC = ({ token="USDC" initialValue={initialAuditContestRewards} onChange={setContestAuditRewards} - placeholder="USDC" + placeholder={token} persistPlaceholder displayTokenLabel={false} /> @@ -651,7 +666,7 @@ export const CreateContestForm: React.FC = ({ token="USDC" initialValue={initialJudgingPrizePool} onChange={setContestJudgingPrizePool} - placeholder="USDC" + placeholder={token} persistPlaceholder displayTokenLabel={false} /> @@ -661,7 +676,7 @@ export const CreateContestForm: React.FC = ({ token="USDC" initialValue={initialLeadJudgeFixedPay} onChange={setContestLeadJudgeFixedPay} - placeholder="USDC" + placeholder={token} persistPlaceholder displayTokenLabel={false} /> @@ -671,12 +686,12 @@ export const CreateContestForm: React.FC = ({ token="USDC" initialValue={initialTotalCost} onChange={setContestTotalCost} - placeholder="USDC" + placeholder={token} persistPlaceholder displayTokenLabel={false} /> - {`Admin Fee: ${sherlockFee} USDC`} + {`Admin Fee: ${sherlockFee} ${token}`} = ({ token="USDC" initialValue={initialCustomLswFixedPay} onChange={setCustomLswFixedPay} - placeholder="USDC" + placeholder={token} persistPlaceholder displayTokenLabel={false} /> From 1945ed31a2ef82d1223804f4d6364bec2f1f6b0c Mon Sep 17 00:00:00 2001 From: Fran Rimoldi Date: Wed, 6 Nov 2024 00:47:15 +0700 Subject: [PATCH 79/80] feat: add token and exchange rate --- src/hooks/api/admin/useAdminConfirmContest.ts | 4 ++++ src/hooks/api/admin/useAdminPricing.ts | 3 +++ src/pages/admin/AdminContestsList/CreateContestForm.tsx | 6 ++++++ 3 files changed, 13 insertions(+) diff --git a/src/hooks/api/admin/useAdminConfirmContest.ts b/src/hooks/api/admin/useAdminConfirmContest.ts index 1622a800..6a70f226 100644 --- a/src/hooks/api/admin/useAdminConfirmContest.ts +++ b/src/hooks/api/admin/useAdminConfirmContest.ts @@ -20,6 +20,8 @@ type AdminConfirmContestParams = { private?: boolean requiresKYC?: boolean maxNumberOfParticipants?: number | null + token: string + exchangeRate: number } export const useAdminConfirmContest = () => { @@ -43,6 +45,8 @@ export const useAdminConfirmContest = () => { private: params.private, requires_kyc: params.requiresKYC, max_number_of_participants: params.maxNumberOfParticipants, + token: params.token, + exchange_rate: params.exchangeRate, }, { diff --git a/src/hooks/api/admin/useAdminPricing.ts b/src/hooks/api/admin/useAdminPricing.ts index 3348ee2d..f3a12bf8 100644 --- a/src/hooks/api/admin/useAdminPricing.ts +++ b/src/hooks/api/admin/useAdminPricing.ts @@ -20,6 +20,7 @@ type GetAdminPricingResponse = { total_price: number total_rewards: number type: PricingType + exchangeRate: number }[] type Pricing = { @@ -34,6 +35,7 @@ type Pricing = { recLeadJudgeFixedPay: number recJudgingPrizePool: number length: number + exchangeRate: number } export const adminPricingQueryKey = (nSLOC: number, token: string) => ["contest-variables", nSLOC, token] @@ -62,5 +64,6 @@ export const useAdminPricing = (nSLOC: number, token: string) => recLeadJudgeFixedPay: recommendedPricing.lead_judge_fixed_pay, recJudgingPrizePool: recommendedPricing?.judging_prize_pool, length: minimumPricing.length, + exchangeRate: minimumPricing.exchangeRate, } }) diff --git a/src/pages/admin/AdminContestsList/CreateContestForm.tsx b/src/pages/admin/AdminContestsList/CreateContestForm.tsx index ecf20be8..d937f290 100644 --- a/src/pages/admin/AdminContestsList/CreateContestForm.tsx +++ b/src/pages/admin/AdminContestsList/CreateContestForm.tsx @@ -46,6 +46,8 @@ export type ContestValues = { requiresKYC?: boolean maxNumberOfParticipants?: number | null previousContestId?: number | null + token: string + exchangeRate: number } } @@ -363,6 +365,8 @@ export const CreateContestForm: React.FC = ({ ? parseInt(maxNumberOfParticipants) : null, previousContestId: isUpdateContest ? previousContest : null, + token, + exchangeRate: contestVariables?.exchangeRate ?? 1, }, }) }, [ @@ -389,6 +393,8 @@ export const CreateContestForm: React.FC = ({ hasLimitedContestants, previousContest, isUpdateContest, + token, + contestVariables?.exchangeRate, ]) const isMinimum = useMemo( From 667baa5562c65d20619318d4fa24a60e45dc5c06 Mon Sep 17 00:00:00 2001 From: Fran Rimoldi Date: Thu, 7 Nov 2024 19:56:31 +0700 Subject: [PATCH 80/80] fix: add token and exchange rate to update endpoint --- src/hooks/api/admin/useAdminUpdateContest.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/hooks/api/admin/useAdminUpdateContest.ts b/src/hooks/api/admin/useAdminUpdateContest.ts index 0d9fc273..699a66e0 100644 --- a/src/hooks/api/admin/useAdminUpdateContest.ts +++ b/src/hooks/api/admin/useAdminUpdateContest.ts @@ -20,6 +20,8 @@ type AdminUpdateContestParams = { private?: boolean requiresKYC?: boolean maxNumberOfParticipants?: number | null + token: string + exchangeRate: number } export const useAdminUpdateContest = () => { @@ -41,6 +43,8 @@ export const useAdminUpdateContest = () => { private: params.private, requires_kyc: params.requiresKYC, max_number_of_participants: params.maxNumberOfParticipants, + token: params.token, + exchange_rate: params.exchangeRate, }) } catch (error) { const axiosError = error as AxiosError