From ddb33fd6e198d1025a2e1e00a9552e94cd740fd9 Mon Sep 17 00:00:00 2001 From: tokodev Date: Wed, 28 Feb 2024 01:52:50 +0100 Subject: [PATCH 01/13] Build submissions to complete grants --- .../nextjs/app/api/grants/submit/route.ts | 32 +++++ packages/nextjs/app/my-grants/page.tsx | 123 +++++++++++++++++- packages/nextjs/services/database/grants.ts | 13 ++ 3 files changed, 166 insertions(+), 2 deletions(-) create mode 100644 packages/nextjs/app/api/grants/submit/route.ts diff --git a/packages/nextjs/app/api/grants/submit/route.ts b/packages/nextjs/app/api/grants/submit/route.ts new file mode 100644 index 0000000..8bbfeae --- /dev/null +++ b/packages/nextjs/app/api/grants/submit/route.ts @@ -0,0 +1,32 @@ +import { NextResponse } from "next/server"; +import { submitGrantBuild } from "~~/services/database/grants"; +import { findUserByAddress } from "~~/services/database/users"; + +type ReqBody = { + grantId?: string; + signer?: string; + link?: string; +}; + +export async function POST(req: Request) { + try { + const { grantId, link, signer } = (await req.json()) as ReqBody; + + if (!grantId || !signer || !link) { + return NextResponse.json({ error: "Invalid form details submitted" }, { status: 400 }); + } + + // Verify if the builder is present + const builder = await findUserByAddress(signer); + if (!builder.exists) { + return NextResponse.json({ error: "Only buidlguild builders can submit for grants" }, { status: 401 }); + } + + const submitBuild = await submitGrantBuild(grantId, link); + + return NextResponse.json({ submitBuild }, { status: 201 }); + } catch (e) { + console.error(e); + return NextResponse.json({ error: "Error processing form" }, { status: 500 }); + } +} diff --git a/packages/nextjs/app/my-grants/page.tsx b/packages/nextjs/app/my-grants/page.tsx index ddc3e8c..340a350 100644 --- a/packages/nextjs/app/my-grants/page.tsx +++ b/packages/nextjs/app/my-grants/page.tsx @@ -1,10 +1,15 @@ "use client"; +import { SetStateAction, useState } from "react"; import { NextPage } from "next"; -import useSWR from "swr"; +import useSWR, { mutate } from "swr"; +import useSWRMutation from "swr/mutation"; import { useAccount } from "wagmi"; +import { InformationCircleIcon } from "@heroicons/react/24/outline"; import { GrantData } from "~~/services/database/schema"; import { PROPOSAL_STATUS } from "~~/utils/grants"; +import { notification } from "~~/utils/scaffold-eth"; +import { postMutationFetcher } from "~~/utils/swr"; const badgeBgColor = { [PROPOSAL_STATUS.PROPOSED]: "bg-warning", @@ -14,10 +19,70 @@ const badgeBgColor = { [PROPOSAL_STATUS.REJECTED]: "bg-error", }; -// ToDo. Action (v1.0 = submit grant after completion) +type ReqBody = { + grantId?: string; + status?: string; + signer?: string; + link?: string; +}; + const MyGrants: NextPage = () => { const { address } = useAccount(); const { data: builderGrants, isLoading } = useSWR(address ? `/api/builders/${address}/grants` : null); + const { trigger: submitBuildLink } = useSWRMutation("/api/grants/submit", postMutationFetcher); + + const [modalIsOpen, setModalIsOpen] = useState(false); + const [buildUrl, setBuildUrl] = useState(""); + const [currentGrantId, setCurrentGrantId] = useState(""); + + const [currentGrantTitle, setCurrentGrantTitle] = useState(""); + + const openModal = (grantId: SetStateAction, grantTitle: string) => { + setCurrentGrantId(grantId); + setCurrentGrantTitle(grantTitle); + setModalIsOpen(true); + }; + const closeModal = () => { + setModalIsOpen(false); + }; + const handleBuildUrlChange = (event: React.ChangeEvent) => { + setBuildUrl(event.target.value); + }; + + // TODO: check for a better validation in stackoverflow + const handleSubmit = async () => { + let processedUrl = buildUrl; + + // Add https:// if it's missing + if (!processedUrl.startsWith("http://") && !processedUrl.startsWith("https://")) { + processedUrl = "https://" + processedUrl; + } + + // Replace http:// with https:// + if (processedUrl.startsWith("http://")) { + processedUrl = "https://" + processedUrl.substring(7); + } + + const urlPattern = new RegExp("^(https://app\\.buidlguidl\\.com/build/)[a-z0-9-]+$"); + if (urlPattern.test(processedUrl.toLowerCase())) { + let notificationId; + try { + notificationId = notification.loading("Submitting build URL"); + console.log("Submitting build URL:", processedUrl, "for grant:", currentGrantId, "by:", address); + await submitBuildLink({ grantId: currentGrantId, link: processedUrl, signer: address }); + mutate(`/api/builders/${address}/grants`); + closeModal(); + notification.remove(notificationId); + notification.success("Build URL submitted successfully"); + } catch (error) { + notification.error("Error submitting build URL"); + } finally { + if (notificationId) notification.remove(notificationId); + } + } else { + notification.error("You must submit a valid build URL (https://app.buidlguidl.com/build/...)"); + } + }; return (
@@ -32,8 +97,62 @@ const MyGrants: NextPage = () => {

{grant.description}

{grant.status}

+ {grant.status === PROPOSAL_STATUS.APPROVED && ( + + )}
))} + + {modalIsOpen && ( +
+
e.stopPropagation()} + > + +

{currentGrantTitle}

+
+
+ +

+ First you'll need to register the build in your  + + BuidlGuidl profile + +  and then submit the URL of your BG build in this form. BG Grants team will review to complete + the grant. +

+
+
+ + + + +
+
+ )} ); }; diff --git a/packages/nextjs/services/database/grants.ts b/packages/nextjs/services/database/grants.ts index e8a1df1..652ac9e 100644 --- a/packages/nextjs/services/database/grants.ts +++ b/packages/nextjs/services/database/grants.ts @@ -145,3 +145,16 @@ export const getGrantsStats = async () => { throw error; } }; + +export const submitGrantBuild = async (grantId: string, link: string) => { + const status = PROPOSAL_STATUS.SUBMITTED; + const grantActionTimeStamp = new Date().getTime(); + const grantActionTimeStampKey = (status + "At") as `${typeof status}At`; + + try { + await grantsCollection.doc(grantId).update({ status, link, [grantActionTimeStampKey]: grantActionTimeStamp }); + } catch (error) { + console.error("Error updating the grant status:", error); + throw error; + } +}; From 9d285644beb7a32ef51912974beaff9733a3836f Mon Sep 17 00:00:00 2001 From: tokodev Date: Wed, 28 Feb 2024 01:56:11 +0100 Subject: [PATCH 02/13] Add build link for the admin to review it --- packages/nextjs/app/admin/_components/GrantReview.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/nextjs/app/admin/_components/GrantReview.tsx b/packages/nextjs/app/admin/_components/GrantReview.tsx index 13cab66..320b805 100644 --- a/packages/nextjs/app/admin/_components/GrantReview.tsx +++ b/packages/nextjs/app/admin/_components/GrantReview.tsx @@ -101,6 +101,11 @@ export const GrantReview = ({ grant }: { grant: GrantDataWithBuilder }) => {

{grant.title} ({grant.id}) + {grant.link && ( + + View Build + + )}

Date: Wed, 28 Feb 2024 11:08:51 +0100 Subject: [PATCH 03/13] Use daisyui alert and tweak design a bit --- packages/nextjs/app/my-grants/page.tsx | 44 ++++++++++++++------------ 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/packages/nextjs/app/my-grants/page.tsx b/packages/nextjs/app/my-grants/page.tsx index 340a350..a58974d 100644 --- a/packages/nextjs/app/my-grants/page.tsx +++ b/packages/nextjs/app/my-grants/page.tsx @@ -107,38 +107,40 @@ const MyGrants: NextPage = () => { {modalIsOpen && (
e.stopPropagation()} > -

{currentGrantTitle}

-
-
- -

- First you'll need to register the build in your  - - BuidlGuidl profile - -  and then submit the URL of your BG build in this form. BG Grants team will review to complete - the grant. -

-
+
+ + + First you'll need to register the build in your  + + BuidlGuidl profile + +  and then submit the URL of your BG build in this form. BG Grants team will review it to complete + the grant. +
- Date: Wed, 28 Feb 2024 11:33:22 +0100 Subject: [PATCH 04/13] Small tweaks --- packages/nextjs/app/api/grants/submit/route.ts | 5 ++++- packages/nextjs/app/my-grants/page.tsx | 8 +++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/nextjs/app/api/grants/submit/route.ts b/packages/nextjs/app/api/grants/submit/route.ts index 8bbfeae..cb9a486 100644 --- a/packages/nextjs/app/api/grants/submit/route.ts +++ b/packages/nextjs/app/api/grants/submit/route.ts @@ -19,7 +19,10 @@ export async function POST(req: Request) { // Verify if the builder is present const builder = await findUserByAddress(signer); if (!builder.exists) { - return NextResponse.json({ error: "Only buidlguild builders can submit for grants" }, { status: 401 }); + return NextResponse.json( + { error: "Only buidlguild builders can submit their builds for active grants" }, + { status: 401 }, + ); } const submitBuild = await submitGrantBuild(grantId, link); diff --git a/packages/nextjs/app/my-grants/page.tsx b/packages/nextjs/app/my-grants/page.tsx index a58974d..1678662 100644 --- a/packages/nextjs/app/my-grants/page.tsx +++ b/packages/nextjs/app/my-grants/page.tsx @@ -53,22 +53,20 @@ const MyGrants: NextPage = () => { const handleSubmit = async () => { let processedUrl = buildUrl; - // Add https:// if it's missing if (!processedUrl.startsWith("http://") && !processedUrl.startsWith("https://")) { processedUrl = "https://" + processedUrl; } - // Replace http:// with https:// if (processedUrl.startsWith("http://")) { processedUrl = "https://" + processedUrl.substring(7); } const urlPattern = new RegExp("^(https://app\\.buidlguidl\\.com/build/)[a-z0-9-]+$"); + if (urlPattern.test(processedUrl.toLowerCase())) { let notificationId; try { notificationId = notification.loading("Submitting build URL"); - console.log("Submitting build URL:", processedUrl, "for grant:", currentGrantId, "by:", address); await submitBuildLink({ grantId: currentGrantId, link: processedUrl, signer: address }); mutate(`/api/builders/${address}/grants`); closeModal(); @@ -118,8 +116,8 @@ const MyGrants: NextPage = () => { Close

{currentGrantTitle}

-
- +
+ Date: Thu, 29 Feb 2024 18:04:34 +0530 Subject: [PATCH 05/13] move modal to separate component --- .../nextjs/app/api/grants/submit/route.ts | 1 + .../app/my-grants/_components/SubmitModal.tsx | 97 +++++++++++++++ packages/nextjs/app/my-grants/page.tsx | 110 +----------------- 3 files changed, 104 insertions(+), 104 deletions(-) create mode 100644 packages/nextjs/app/my-grants/_components/SubmitModal.tsx diff --git a/packages/nextjs/app/api/grants/submit/route.ts b/packages/nextjs/app/api/grants/submit/route.ts index cb9a486..9e7f8b3 100644 --- a/packages/nextjs/app/api/grants/submit/route.ts +++ b/packages/nextjs/app/api/grants/submit/route.ts @@ -8,6 +8,7 @@ type ReqBody = { link?: string; }; +// TODO: Check if the grants is owned by the builder export async function POST(req: Request) { try { const { grantId, link, signer } = (await req.json()) as ReqBody; diff --git a/packages/nextjs/app/my-grants/_components/SubmitModal.tsx b/packages/nextjs/app/my-grants/_components/SubmitModal.tsx new file mode 100644 index 0000000..093fae8 --- /dev/null +++ b/packages/nextjs/app/my-grants/_components/SubmitModal.tsx @@ -0,0 +1,97 @@ +import { useState } from "react"; +import { mutate } from "swr"; +import useSWRMutation from "swr/mutation"; +import { useAccount } from "wagmi"; +import { InformationCircleIcon } from "@heroicons/react/24/outline"; +import { notification } from "~~/utils/scaffold-eth"; +import { postMutationFetcher } from "~~/utils/swr"; + +type ReqBody = { + grantId?: string; + status?: string; + signer?: string; + link?: string; +}; + +export const SubmitModal = ({ + grantTitle, + grantId, + closeModal, +}: { + grantTitle: string; + grantId: string; + closeModal: () => void; +}) => { + const { address: connectedAddress } = useAccount(); + const [buildUrl, setBuildUrl] = useState(""); + const { trigger: submitBuildLink } = useSWRMutation("/api/grants/submit", postMutationFetcher); + + const handleSubmit = async () => { + const urlPattern = new RegExp("^(https://app\\.buidlguidl\\.com/build/)[a-z0-9-]+$"); + + if (!urlPattern.test(buildUrl.toLowerCase())) + return notification.error("You must submit a valid build URL (https://app.buidlguidl.com/build/...)"); + + let notificationId; + try { + notificationId = notification.loading("Submitting build URL"); + await submitBuildLink({ grantId: grantId, link: buildUrl, signer: connectedAddress }); + await mutate(`/api/builders/${connectedAddress}/grants`); + closeModal(); + notification.remove(notificationId); + notification.success("Build URL submitted successfully"); + } catch (error) { + notification.error("Error submitting build URL"); + } finally { + if (notificationId) notification.remove(notificationId); + } + }; + + return ( +
+
e.stopPropagation()}> + +

{grantTitle}

+
+ + + First you'll need to register the build in your  + + BuidlGuidl profile + +  and then submit the URL of your BG build in this form. BG Grants team will review it to complete the + grant. + +
+ + setBuildUrl(e.target.value)} + placeholder="https://app.buidlguidl.com/build/..." + className="placeholder: pl-[14px] mt-4 w-full p-1 rounded-lg" + /> + +
+
+ ); +}; diff --git a/packages/nextjs/app/my-grants/page.tsx b/packages/nextjs/app/my-grants/page.tsx index 1678662..a363887 100644 --- a/packages/nextjs/app/my-grants/page.tsx +++ b/packages/nextjs/app/my-grants/page.tsx @@ -1,16 +1,14 @@ "use client"; -import { SetStateAction, useState } from "react"; +import { useState } from "react"; +import { SubmitModal } from "./_components/SubmitModal"; import { NextPage } from "next"; -import useSWR, { mutate } from "swr"; -import useSWRMutation from "swr/mutation"; +import useSWR from "swr"; import { useAccount } from "wagmi"; -import { InformationCircleIcon } from "@heroicons/react/24/outline"; import { GrantData } from "~~/services/database/schema"; import { PROPOSAL_STATUS } from "~~/utils/grants"; -import { notification } from "~~/utils/scaffold-eth"; -import { postMutationFetcher } from "~~/utils/swr"; +// TODO: Move this to util const badgeBgColor = { [PROPOSAL_STATUS.PROPOSED]: "bg-warning", [PROPOSAL_STATUS.APPROVED]: "bg-success", @@ -19,68 +17,19 @@ const badgeBgColor = { [PROPOSAL_STATUS.REJECTED]: "bg-error", }; -type ReqBody = { - grantId?: string; - status?: string; - signer?: string; - link?: string; -}; - const MyGrants: NextPage = () => { const { address } = useAccount(); const { data: builderGrants, isLoading } = useSWR(address ? `/api/builders/${address}/grants` : null); - const { trigger: submitBuildLink } = useSWRMutation("/api/grants/submit", postMutationFetcher); const [modalIsOpen, setModalIsOpen] = useState(false); - const [buildUrl, setBuildUrl] = useState(""); const [currentGrantId, setCurrentGrantId] = useState(""); - const [currentGrantTitle, setCurrentGrantTitle] = useState(""); - const openModal = (grantId: SetStateAction, grantTitle: string) => { + const openModal = (grantId: string, grantTitle: string) => { setCurrentGrantId(grantId); setCurrentGrantTitle(grantTitle); setModalIsOpen(true); }; - const closeModal = () => { - setModalIsOpen(false); - }; - const handleBuildUrlChange = (event: React.ChangeEvent) => { - setBuildUrl(event.target.value); - }; - - // TODO: check for a better validation in stackoverflow - const handleSubmit = async () => { - let processedUrl = buildUrl; - - if (!processedUrl.startsWith("http://") && !processedUrl.startsWith("https://")) { - processedUrl = "https://" + processedUrl; - } - - if (processedUrl.startsWith("http://")) { - processedUrl = "https://" + processedUrl.substring(7); - } - - const urlPattern = new RegExp("^(https://app\\.buidlguidl\\.com/build/)[a-z0-9-]+$"); - - if (urlPattern.test(processedUrl.toLowerCase())) { - let notificationId; - try { - notificationId = notification.loading("Submitting build URL"); - await submitBuildLink({ grantId: currentGrantId, link: processedUrl, signer: address }); - mutate(`/api/builders/${address}/grants`); - closeModal(); - notification.remove(notificationId); - notification.success("Build URL submitted successfully"); - } catch (error) { - notification.error("Error submitting build URL"); - } finally { - if (notificationId) notification.remove(notificationId); - } - } else { - notification.error("You must submit a valid build URL (https://app.buidlguidl.com/build/...)"); - } - }; return (
@@ -104,54 +53,7 @@ const MyGrants: NextPage = () => { ))} {modalIsOpen && ( -
-
e.stopPropagation()} - > - -

{currentGrantTitle}

-
- - - First you'll need to register the build in your  - - BuidlGuidl profile - -  and then submit the URL of your BG build in this form. BG Grants team will review it to complete - the grant. - -
- - - -
-
+ setModalIsOpen(false)} /> )}
); From 7c900f3ac515e4029fcfc998d402391114185440 Mon Sep 17 00:00:00 2001 From: Shiv Bhonde Date: Thu, 29 Feb 2024 18:37:49 +0530 Subject: [PATCH 06/13] add EIP712 signature --- .../nextjs/app/api/grants/submit/route.ts | 27 +++++++++++++-- .../app/my-grants/_components/SubmitModal.tsx | 33 ++++++++++++------- packages/nextjs/app/my-grants/page.tsx | 14 ++++---- packages/nextjs/utils/eip712.ts | 8 +++++ 4 files changed, 60 insertions(+), 22 deletions(-) diff --git a/packages/nextjs/app/api/grants/submit/route.ts b/packages/nextjs/app/api/grants/submit/route.ts index 9e7f8b3..a8650ff 100644 --- a/packages/nextjs/app/api/grants/submit/route.ts +++ b/packages/nextjs/app/api/grants/submit/route.ts @@ -1,22 +1,45 @@ import { NextResponse } from "next/server"; +import { recoverTypedDataAddress } from "viem"; import { submitGrantBuild } from "~~/services/database/grants"; import { findUserByAddress } from "~~/services/database/users"; +import { EIP_712_DOMAIN, EIP_712_TYPES__SUBMIT_GRANT } from "~~/utils/eip712"; +import { PROPOSAL_STATUS } from "~~/utils/grants"; type ReqBody = { grantId?: string; signer?: string; link?: string; + signature?: `0x${string}`; }; // TODO: Check if the grants is owned by the builder +// TODO: Check if the grant status is accepted export async function POST(req: Request) { try { - const { grantId, link, signer } = (await req.json()) as ReqBody; + const { grantId, link, signer, signature } = (await req.json()) as ReqBody; - if (!grantId || !signer || !link) { + if (!grantId || !signer || !link || !signature) { return NextResponse.json({ error: "Invalid form details submitted" }, { status: 400 }); } + // Validate Signature + const recoveredAddress = await recoverTypedDataAddress({ + domain: EIP_712_DOMAIN, + types: EIP_712_TYPES__SUBMIT_GRANT, + primaryType: "Message", + message: { + grantId: grantId, + action: PROPOSAL_STATUS.SUBMITTED, + link, + }, + signature, + }); + + if (recoveredAddress !== signer) { + console.error("Signature error", recoveredAddress, signer); + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + // Verify if the builder is present const builder = await findUserByAddress(signer); if (!builder.exists) { diff --git a/packages/nextjs/app/my-grants/_components/SubmitModal.tsx b/packages/nextjs/app/my-grants/_components/SubmitModal.tsx index 093fae8..a4421f3 100644 --- a/packages/nextjs/app/my-grants/_components/SubmitModal.tsx +++ b/packages/nextjs/app/my-grants/_components/SubmitModal.tsx @@ -1,8 +1,11 @@ import { useState } from "react"; import { mutate } from "swr"; import useSWRMutation from "swr/mutation"; -import { useAccount } from "wagmi"; +import { useAccount, useSignTypedData } from "wagmi"; import { InformationCircleIcon } from "@heroicons/react/24/outline"; +import { GrantData } from "~~/services/database/schema"; +import { EIP_712_DOMAIN, EIP_712_TYPES__SUBMIT_GRANT } from "~~/utils/eip712"; +import { PROPOSAL_STATUS } from "~~/utils/grants"; import { notification } from "~~/utils/scaffold-eth"; import { postMutationFetcher } from "~~/utils/swr"; @@ -11,31 +14,37 @@ type ReqBody = { status?: string; signer?: string; link?: string; + signature?: `0x${string}`; }; -export const SubmitModal = ({ - grantTitle, - grantId, - closeModal, -}: { - grantTitle: string; - grantId: string; - closeModal: () => void; -}) => { +export const SubmitModal = ({ grant, closeModal }: { grant: GrantData; closeModal: () => void }) => { const { address: connectedAddress } = useAccount(); const [buildUrl, setBuildUrl] = useState(""); const { trigger: submitBuildLink } = useSWRMutation("/api/grants/submit", postMutationFetcher); + const { signTypedDataAsync } = useSignTypedData(); + const handleSubmit = async () => { const urlPattern = new RegExp("^(https://app\\.buidlguidl\\.com/build/)[a-z0-9-]+$"); if (!urlPattern.test(buildUrl.toLowerCase())) return notification.error("You must submit a valid build URL (https://app.buidlguidl.com/build/...)"); + const signature = await signTypedDataAsync({ + domain: EIP_712_DOMAIN, + types: EIP_712_TYPES__SUBMIT_GRANT, + primaryType: "Message", + message: { + grantId: grant.id, + action: PROPOSAL_STATUS.SUBMITTED, + link: buildUrl, + }, + }); + let notificationId; try { notificationId = notification.loading("Submitting build URL"); - await submitBuildLink({ grantId: grantId, link: buildUrl, signer: connectedAddress }); + await submitBuildLink({ grantId: grant.id, link: buildUrl, signer: connectedAddress, signature }); await mutate(`/api/builders/${connectedAddress}/grants`); closeModal(); notification.remove(notificationId); @@ -56,7 +65,7 @@ export const SubmitModal = ({ -

{grantTitle}

+

{grant.title}

{ const { data: builderGrants, isLoading } = useSWR(address ? `/api/builders/${address}/grants` : null); const [modalIsOpen, setModalIsOpen] = useState(false); - const [currentGrantId, setCurrentGrantId] = useState(""); - const [currentGrantTitle, setCurrentGrantTitle] = useState(""); + const [currentGrant, setCurrentGrant] = useState(null); - const openModal = (grantId: string, grantTitle: string) => { - setCurrentGrantId(grantId); - setCurrentGrantTitle(grantTitle); + const openModal = (grant: GrantData) => { + setCurrentGrant(grant); setModalIsOpen(true); }; @@ -45,15 +43,15 @@ const MyGrants: NextPage = () => {

{grant.description}

{grant.status}

{grant.status === PROPOSAL_STATUS.APPROVED && ( - )}
))} - {modalIsOpen && ( - setModalIsOpen(false)} /> + {modalIsOpen && currentGrant !== null && ( + setModalIsOpen(false)} /> )}
); diff --git a/packages/nextjs/utils/eip712.ts b/packages/nextjs/utils/eip712.ts index fdc67ab..319ecee 100644 --- a/packages/nextjs/utils/eip712.ts +++ b/packages/nextjs/utils/eip712.ts @@ -19,3 +19,11 @@ export const EIP_712_TYPES__REVIEW_GRANT = { { name: "action", type: "string" }, ], } as const; + +export const EIP_712_TYPES__SUBMIT_GRANT = { + Message: [ + { name: "grantId", type: "string" }, + { name: "action", type: "string" }, + { name: "link", type: "string" }, + ], +} as const; From ccebc2b1aa112bb2d33f60ac56f1a6d78945a84a Mon Sep 17 00:00:00 2001 From: Shiv Bhonde Date: Thu, 29 Feb 2024 19:00:59 +0530 Subject: [PATCH 07/13] add extra authorization setps before updating link and status --- .../nextjs/app/api/grants/submit/route.ts | 26 ++++++------ .../app/my-grants/_components/SubmitModal.tsx | 41 ++++++++++++------- packages/nextjs/services/database/grants.ts | 20 +++++++-- 3 files changed, 56 insertions(+), 31 deletions(-) diff --git a/packages/nextjs/app/api/grants/submit/route.ts b/packages/nextjs/app/api/grants/submit/route.ts index a8650ff..1877049 100644 --- a/packages/nextjs/app/api/grants/submit/route.ts +++ b/packages/nextjs/app/api/grants/submit/route.ts @@ -1,7 +1,6 @@ import { NextResponse } from "next/server"; import { recoverTypedDataAddress } from "viem"; -import { submitGrantBuild } from "~~/services/database/grants"; -import { findUserByAddress } from "~~/services/database/users"; +import { getGrantById, submitGrantBuild } from "~~/services/database/grants"; import { EIP_712_DOMAIN, EIP_712_TYPES__SUBMIT_GRANT } from "~~/utils/eip712"; import { PROPOSAL_STATUS } from "~~/utils/grants"; @@ -12,12 +11,9 @@ type ReqBody = { signature?: `0x${string}`; }; -// TODO: Check if the grants is owned by the builder -// TODO: Check if the grant status is accepted export async function POST(req: Request) { try { const { grantId, link, signer, signature } = (await req.json()) as ReqBody; - if (!grantId || !signer || !link || !signature) { return NextResponse.json({ error: "Invalid form details submitted" }, { status: 400 }); } @@ -29,24 +25,26 @@ export async function POST(req: Request) { primaryType: "Message", message: { grantId: grantId, - action: PROPOSAL_STATUS.SUBMITTED, + action: "submit", link, }, signature, }); - if (recoveredAddress !== signer) { console.error("Signature error", recoveredAddress, signer); return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } - // Verify if the builder is present - const builder = await findUserByAddress(signer); - if (!builder.exists) { - return NextResponse.json( - { error: "Only buidlguild builders can submit their builds for active grants" }, - { status: 401 }, - ); + // Validate if the grant is approved and owned by the builder + const grant = await getGrantById(grantId); + if (!grant) { + return NextResponse.json({ error: "Invalid grant" }, { status: 400 }); + } + if (grant.builder !== signer) { + return NextResponse.json({ error: "Builder does not own this grant" }, { status: 401 }); + } + if (grant.status !== PROPOSAL_STATUS.APPROVED) { + return NextResponse.json({ error: "Grant is not approved" }, { status: 400 }); } const submitBuild = await submitGrantBuild(grantId, link); diff --git a/packages/nextjs/app/my-grants/_components/SubmitModal.tsx b/packages/nextjs/app/my-grants/_components/SubmitModal.tsx index a4421f3..1c1f522 100644 --- a/packages/nextjs/app/my-grants/_components/SubmitModal.tsx +++ b/packages/nextjs/app/my-grants/_components/SubmitModal.tsx @@ -5,7 +5,6 @@ import { useAccount, useSignTypedData } from "wagmi"; import { InformationCircleIcon } from "@heroicons/react/24/outline"; import { GrantData } from "~~/services/database/schema"; import { EIP_712_DOMAIN, EIP_712_TYPES__SUBMIT_GRANT } from "~~/utils/eip712"; -import { PROPOSAL_STATUS } from "~~/utils/grants"; import { notification } from "~~/utils/scaffold-eth"; import { postMutationFetcher } from "~~/utils/swr"; @@ -20,9 +19,12 @@ type ReqBody = { export const SubmitModal = ({ grant, closeModal }: { grant: GrantData; closeModal: () => void }) => { const { address: connectedAddress } = useAccount(); const [buildUrl, setBuildUrl] = useState(""); - const { trigger: submitBuildLink } = useSWRMutation("/api/grants/submit", postMutationFetcher); + const { trigger: submitBuildLink, isMutating: isSubmittingLink } = useSWRMutation( + "/api/grants/submit", + postMutationFetcher, + ); - const { signTypedDataAsync } = useSignTypedData(); + const { signTypedDataAsync, isLoading: isSigningMessage } = useSignTypedData(); const handleSubmit = async () => { const urlPattern = new RegExp("^(https://app\\.buidlguidl\\.com/build/)[a-z0-9-]+$"); @@ -30,16 +32,23 @@ export const SubmitModal = ({ grant, closeModal }: { grant: GrantData; closeModa if (!urlPattern.test(buildUrl.toLowerCase())) return notification.error("You must submit a valid build URL (https://app.buidlguidl.com/build/...)"); - const signature = await signTypedDataAsync({ - domain: EIP_712_DOMAIN, - types: EIP_712_TYPES__SUBMIT_GRANT, - primaryType: "Message", - message: { - grantId: grant.id, - action: PROPOSAL_STATUS.SUBMITTED, - link: buildUrl, - }, - }); + let signature; + try { + signature = await signTypedDataAsync({ + domain: EIP_712_DOMAIN, + types: EIP_712_TYPES__SUBMIT_GRANT, + primaryType: "Message", + message: { + grantId: grant.id, + action: "submit", + link: buildUrl, + }, + }); + } catch (error) { + console.error("Error signing message", error); + notification.error("Error signing message"); + return; + } let notificationId; try { @@ -97,7 +106,11 @@ export const SubmitModal = ({ grant, closeModal }: { grant: GrantData; closeModa placeholder="https://app.buidlguidl.com/build/..." className="placeholder: pl-[14px] mt-4 w-full p-1 rounded-lg" /> -
diff --git a/packages/nextjs/services/database/grants.ts b/packages/nextjs/services/database/grants.ts index 652ac9e..7a957ed 100644 --- a/packages/nextjs/services/database/grants.ts +++ b/packages/nextjs/services/database/grants.ts @@ -5,8 +5,8 @@ import { PROPOSAL_STATUS, ProposalStatusType } from "~~/utils/grants"; const firestoreDB = getFirestoreConnector(); const grantsCollection = firestoreDB.collection("grants"); -// const getGrantsDoc = (id: string) => grantsCollection.doc(id); -// const getGrantSnapshotById = (id: string) => getGrantsDoc(id).get(); +const getGrantsDoc = (id: string) => grantsCollection.doc(id); +const getGrantSnapshotById = (id: string) => getGrantsDoc(id).get(); export const createGrant = async (grantData: Omit) => { try { @@ -146,13 +146,27 @@ export const getGrantsStats = async () => { } }; +export const getGrantById = async (grantId: string) => { + try { + const grantSnapshot = await getGrantSnapshotById(grantId); + // TODO: Verify if `exists` value is really provided by firebase + if (!grantSnapshot.exists) { + return null; + } + return { id: grantSnapshot.id, ...grantSnapshot.data() } as GrantData; + } catch (error) { + console.error("Error getting grant by id:", error); + throw error; + } +}; + export const submitGrantBuild = async (grantId: string, link: string) => { const status = PROPOSAL_STATUS.SUBMITTED; const grantActionTimeStamp = new Date().getTime(); const grantActionTimeStampKey = (status + "At") as `${typeof status}At`; try { - await grantsCollection.doc(grantId).update({ status, link, [grantActionTimeStampKey]: grantActionTimeStamp }); + await getGrantsDoc(grantId).update({ status, link, [grantActionTimeStampKey]: grantActionTimeStamp }); } catch (error) { console.error("Error updating the grant status:", error); throw error; From 8153797fa25d3351f7d0cd2a3a82398af2c61901 Mon Sep 17 00:00:00 2001 From: Shiv Bhonde Date: Thu, 29 Feb 2024 19:03:49 +0530 Subject: [PATCH 08/13] add todo about verifying submitted build link --- packages/nextjs/app/api/grants/submit/route.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/nextjs/app/api/grants/submit/route.ts b/packages/nextjs/app/api/grants/submit/route.ts index 1877049..398a073 100644 --- a/packages/nextjs/app/api/grants/submit/route.ts +++ b/packages/nextjs/app/api/grants/submit/route.ts @@ -11,6 +11,8 @@ type ReqBody = { signature?: `0x${string}`; }; +// TODO: Maybe we could fetch build from firebase and check if its really present, +// also check that build was actually submitted by the same builder export async function POST(req: Request) { try { const { grantId, link, signer, signature } = (await req.json()) as ReqBody; From 5a28d53c4d6234a4bacb71a46835415fedde27e9 Mon Sep 17 00:00:00 2001 From: Shiv Bhonde Date: Thu, 29 Feb 2024 19:12:47 +0530 Subject: [PATCH 09/13] send empty response if request was sucessfull --- packages/nextjs/app/api/grants/submit/route.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/nextjs/app/api/grants/submit/route.ts b/packages/nextjs/app/api/grants/submit/route.ts index 398a073..88a10e3 100644 --- a/packages/nextjs/app/api/grants/submit/route.ts +++ b/packages/nextjs/app/api/grants/submit/route.ts @@ -49,9 +49,9 @@ export async function POST(req: Request) { return NextResponse.json({ error: "Grant is not approved" }, { status: 400 }); } - const submitBuild = await submitGrantBuild(grantId, link); + await submitGrantBuild(grantId, link); - return NextResponse.json({ submitBuild }, { status: 201 }); + return NextResponse.json({}, { status: 201 }); } catch (e) { console.error(e); return NextResponse.json({ error: "Error processing form" }, { status: 500 }); From 132d539ac4bb1330803cb731da9d7e46fed2c6d1 Mon Sep 17 00:00:00 2001 From: Shiv Bhonde Date: Thu, 29 Feb 2024 19:16:10 +0530 Subject: [PATCH 10/13] remove util todo --- packages/nextjs/app/my-grants/page.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/nextjs/app/my-grants/page.tsx b/packages/nextjs/app/my-grants/page.tsx index 31a831a..5d65176 100644 --- a/packages/nextjs/app/my-grants/page.tsx +++ b/packages/nextjs/app/my-grants/page.tsx @@ -8,7 +8,6 @@ import { useAccount } from "wagmi"; import { GrantData } from "~~/services/database/schema"; import { PROPOSAL_STATUS } from "~~/utils/grants"; -// TODO: Move this to util const badgeBgColor = { [PROPOSAL_STATUS.PROPOSED]: "bg-warning", [PROPOSAL_STATUS.APPROVED]: "bg-success", From 280e955927fbf385b8a6c3e4e1ac2e88ff1f11b8 Mon Sep 17 00:00:00 2001 From: Shiv Bhonde Date: Thu, 29 Feb 2024 19:30:22 +0530 Subject: [PATCH 11/13] update todo comment --- packages/nextjs/app/api/grants/submit/route.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nextjs/app/api/grants/submit/route.ts b/packages/nextjs/app/api/grants/submit/route.ts index 88a10e3..93670ac 100644 --- a/packages/nextjs/app/api/grants/submit/route.ts +++ b/packages/nextjs/app/api/grants/submit/route.ts @@ -11,7 +11,7 @@ type ReqBody = { signature?: `0x${string}`; }; -// TODO: Maybe we could fetch build from firebase and check if its really present, +// TODO: Maybe we could fetch submmited build on BG app from firebase and check if its really present, // also check that build was actually submitted by the same builder export async function POST(req: Request) { try { From 9667c746053c918e291e2935550a572eeb95ae9b Mon Sep 17 00:00:00 2001 From: Shiv Bhonde Date: Thu, 29 Feb 2024 19:32:14 +0530 Subject: [PATCH 12/13] remove `exists` todo its actually provided by firebase --- packages/nextjs/services/database/grants.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/nextjs/services/database/grants.ts b/packages/nextjs/services/database/grants.ts index 7a957ed..014cd22 100644 --- a/packages/nextjs/services/database/grants.ts +++ b/packages/nextjs/services/database/grants.ts @@ -149,7 +149,6 @@ export const getGrantsStats = async () => { export const getGrantById = async (grantId: string) => { try { const grantSnapshot = await getGrantSnapshotById(grantId); - // TODO: Verify if `exists` value is really provided by firebase if (!grantSnapshot.exists) { return null; } From f59987f126a8aab64b952a268f01c499998a57ce Mon Sep 17 00:00:00 2001 From: Shiv Bhonde Date: Fri, 1 Mar 2024 21:22:02 +0530 Subject: [PATCH 13/13] remove /submit route todo --- packages/nextjs/app/api/grants/submit/route.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/nextjs/app/api/grants/submit/route.ts b/packages/nextjs/app/api/grants/submit/route.ts index 93670ac..fff3fce 100644 --- a/packages/nextjs/app/api/grants/submit/route.ts +++ b/packages/nextjs/app/api/grants/submit/route.ts @@ -11,8 +11,6 @@ type ReqBody = { signature?: `0x${string}`; }; -// TODO: Maybe we could fetch submmited build on BG app from firebase and check if its really present, -// also check that build was actually submitted by the same builder export async function POST(req: Request) { try { const { grantId, link, signer, signature } = (await req.json()) as ReqBody;