From bd33702bbacc1e008e07357a7e6a801b7efbdbc2 Mon Sep 17 00:00:00 2001
From: Pablo Alayeto <55535804+Pabl0cks@users.noreply.github.com>
Date: Fri, 1 Mar 2024 17:08:55 +0100
Subject: [PATCH] Submit completed builds (#50)
---
.../app/admin/_components/GrantReview.tsx | 5 +
.../nextjs/app/api/grants/submit/route.ts | 57 +++++++++
.../app/my-grants/_components/SubmitModal.tsx | 119 ++++++++++++++++++
packages/nextjs/app/my-grants/page.tsx | 20 ++-
packages/nextjs/services/database/grants.ts | 30 ++++-
packages/nextjs/utils/eip712.ts | 8 ++
6 files changed, 236 insertions(+), 3 deletions(-)
create mode 100644 packages/nextjs/app/api/grants/submit/route.ts
create mode 100644 packages/nextjs/app/my-grants/_components/SubmitModal.tsx
diff --git a/packages/nextjs/app/admin/_components/GrantReview.tsx b/packages/nextjs/app/admin/_components/GrantReview.tsx
index da31696..b46ddbf 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 }) => {
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..fff3fce
--- /dev/null
+++ b/packages/nextjs/app/api/grants/submit/route.ts
@@ -0,0 +1,57 @@
+import { NextResponse } from "next/server";
+import { recoverTypedDataAddress } from "viem";
+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";
+
+type ReqBody = {
+ grantId?: string;
+ signer?: string;
+ link?: string;
+ signature?: `0x${string}`;
+};
+
+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 });
+ }
+
+ // Validate Signature
+ const recoveredAddress = await recoverTypedDataAddress({
+ domain: EIP_712_DOMAIN,
+ types: EIP_712_TYPES__SUBMIT_GRANT,
+ primaryType: "Message",
+ message: {
+ grantId: grantId,
+ action: "submit",
+ link,
+ },
+ signature,
+ });
+ if (recoveredAddress !== signer) {
+ console.error("Signature error", recoveredAddress, signer);
+ return NextResponse.json({ error: "Unauthorized" }, { 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 });
+ }
+
+ await submitGrantBuild(grantId, link);
+
+ return NextResponse.json({}, { status: 201 });
+ } catch (e) {
+ console.error(e);
+ return NextResponse.json({ error: "Error processing form" }, { status: 500 });
+ }
+}
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..1c1f522
--- /dev/null
+++ b/packages/nextjs/app/my-grants/_components/SubmitModal.tsx
@@ -0,0 +1,119 @@
+import { useState } from "react";
+import { mutate } from "swr";
+import useSWRMutation from "swr/mutation";
+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 { notification } from "~~/utils/scaffold-eth";
+import { postMutationFetcher } from "~~/utils/swr";
+
+type ReqBody = {
+ grantId?: string;
+ status?: string;
+ signer?: string;
+ link?: string;
+ signature?: `0x${string}`;
+};
+
+export const SubmitModal = ({ grant, closeModal }: { grant: GrantData; closeModal: () => void }) => {
+ const { address: connectedAddress } = useAccount();
+ const [buildUrl, setBuildUrl] = useState("");
+ const { trigger: submitBuildLink, isMutating: isSubmittingLink } = useSWRMutation(
+ "/api/grants/submit",
+ postMutationFetcher
,
+ );
+
+ const { signTypedDataAsync, isLoading: isSigningMessage } = 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/...)");
+
+ 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 {
+ notificationId = notification.loading("Submitting build URL");
+ await submitBuildLink({ grantId: grant.id, link: buildUrl, signer: connectedAddress, signature });
+ 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()}>
+
+ Close
+
+
{grant.title}
+
+
+
+ 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.
+
+
+
+ Build URL to be reviewed and complete grant:
+
+
setBuildUrl(e.target.value)}
+ placeholder="https://app.buidlguidl.com/build/..."
+ className="placeholder: pl-[14px] mt-4 w-full p-1 rounded-lg"
+ />
+
+ Submit
+
+
+
+ );
+};
diff --git a/packages/nextjs/app/my-grants/page.tsx b/packages/nextjs/app/my-grants/page.tsx
index ddc3e8c..5d65176 100644
--- a/packages/nextjs/app/my-grants/page.tsx
+++ b/packages/nextjs/app/my-grants/page.tsx
@@ -1,5 +1,7 @@
"use client";
+import { useState } from "react";
+import { SubmitModal } from "./_components/SubmitModal";
import { NextPage } from "next";
import useSWR from "swr";
import { useAccount } from "wagmi";
@@ -14,11 +16,18 @@ const badgeBgColor = {
[PROPOSAL_STATUS.REJECTED]: "bg-error",
};
-// ToDo. Action (v1.0 = submit grant after completion)
const MyGrants: NextPage = () => {
const { address } = useAccount();
const { data: builderGrants, isLoading } = useSWR(address ? `/api/builders/${address}/grants` : null);
+ const [modalIsOpen, setModalIsOpen] = useState(false);
+ const [currentGrant, setCurrentGrant] = useState(null);
+
+ const openModal = (grant: GrantData) => {
+ setCurrentGrant(grant);
+ setModalIsOpen(true);
+ };
+
return (
My grants
@@ -32,8 +41,17 @@ const MyGrants: NextPage = () => {
{grant.description}
{grant.status}
+ {grant.status === PROPOSAL_STATUS.APPROVED && (
+
openModal(grant)} className="btn btn-primary float-right">
+ Submit build
+
+ )}
))}
+
+ {modalIsOpen && currentGrant !== null && (
+ setModalIsOpen(false)} />
+ )}
);
};
diff --git a/packages/nextjs/services/database/grants.ts b/packages/nextjs/services/database/grants.ts
index 7950453..73dacfa 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