From ae8d7f28167b1a6c1e858ecb7b2243f7c6dc2c2e Mon Sep 17 00:00:00 2001
From: "Shiv Bhonde | shivbhonde.eth"
Date: Mon, 19 Feb 2024 22:59:45 +0530
Subject: [PATCH] use route handler for applying of grants (#20)
---
.../nextjs/app/_components/HomepageHero.tsx | 2 +-
packages/nextjs/app/api/grants/new/route.ts | 54 +++++++++++++++++++
.../_component/Form.tsx | 47 +++++++++++-----
.../_component/SubmitButton.tsx | 0
.../app/{submit-grant => apply}/page.tsx | 0
.../nextjs/app/submit-grant/_actions/index.ts | 51 ------------------
packages/nextjs/utils/eip712.ts | 8 +++
7 files changed, 96 insertions(+), 66 deletions(-)
create mode 100644 packages/nextjs/app/api/grants/new/route.ts
rename packages/nextjs/app/{submit-grant => apply}/_component/Form.tsx (68%)
rename packages/nextjs/app/{submit-grant => apply}/_component/SubmitButton.tsx (100%)
rename packages/nextjs/app/{submit-grant => apply}/page.tsx (100%)
delete mode 100644 packages/nextjs/app/submit-grant/_actions/index.ts
diff --git a/packages/nextjs/app/_components/HomepageHero.tsx b/packages/nextjs/app/_components/HomepageHero.tsx
index f90c7b0..7100f37 100644
--- a/packages/nextjs/app/_components/HomepageHero.tsx
+++ b/packages/nextjs/app/_components/HomepageHero.tsx
@@ -16,7 +16,7 @@ export const HomepageHero = () => (
finished SRE or completed one of our batches? This could be your next step in BuidlGuidl’s journey.
Learn More
diff --git a/packages/nextjs/app/api/grants/new/route.ts b/packages/nextjs/app/api/grants/new/route.ts
new file mode 100644
index 0000000..1a80c8b
--- /dev/null
+++ b/packages/nextjs/app/api/grants/new/route.ts
@@ -0,0 +1,54 @@
+import { NextResponse } from "next/server";
+import { recoverTypedDataAddress } from "viem";
+import { createGrant } from "~~/services/database/grants";
+import { findUserByAddress } from "~~/services/database/users";
+import { EIP_712_DOMAIN, EIP_712_TYPES__APPLY_FOR_GRANT } from "~~/utils/eip712";
+
+type ReqBody = {
+ title?: string;
+ description?: string;
+ askAmount?: string;
+ signature?: `0x${string}`;
+ signer?: string;
+};
+
+// TODO: We could also add extra validtion of nonce
+export async function POST(req: Request) {
+ try {
+ const { title, description, askAmount, signature, signer } = (await req.json()) as ReqBody;
+
+ if (!title || !description || !askAmount || isNaN(Number(askAmount)) || !signature || !signer) {
+ return NextResponse.json({ error: "Invalid form details submited" }, { status: 400 });
+ }
+
+ // Verif 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 recoveredAddress = await recoverTypedDataAddress({
+ domain: EIP_712_DOMAIN,
+ types: EIP_712_TYPES__APPLY_FOR_GRANT,
+ primaryType: "Message",
+ message: { title, description, askAmount },
+ signature: signature,
+ });
+
+ if (recoveredAddress !== signer) {
+ return NextResponse.json({ error: "Recovered address did not match signer" }, { status: 401 });
+ }
+
+ const grant = await createGrant({
+ title: title,
+ description: description,
+ askAmount: Number(askAmount),
+ builder: signer,
+ });
+
+ return NextResponse.json({ grant }, { status: 201 });
+ } catch (e) {
+ console.error(e);
+ return NextResponse.json({ error: "Error processing form" }, { status: 500 });
+ }
+}
diff --git a/packages/nextjs/app/submit-grant/_component/Form.tsx b/packages/nextjs/app/apply/_component/Form.tsx
similarity index 68%
rename from packages/nextjs/app/submit-grant/_component/Form.tsx
rename to packages/nextjs/app/apply/_component/Form.tsx
index 6ca879f..18f4ac2 100644
--- a/packages/nextjs/app/submit-grant/_component/Form.tsx
+++ b/packages/nextjs/app/apply/_component/Form.tsx
@@ -1,35 +1,53 @@
"use client";
import { useRouter } from "next/navigation";
-import { submitGrantAction } from "../_actions";
import SubmitButton from "./SubmitButton";
-import { useAccount, useSignMessage } from "wagmi";
+import { useAccount, useSignTypedData } from "wagmi";
+import { EIP_712_DOMAIN, EIP_712_TYPES__APPLY_FOR_GRANT } from "~~/utils/eip712";
import { notification } from "~~/utils/scaffold-eth";
const selectOptions = [0.1, 0.25, 0.5, 1];
const Form = () => {
- const { signMessageAsync } = useSignMessage();
const { address: connectedAddress } = useAccount();
+ const { signTypedDataAsync } = useSignTypedData();
const router = useRouter();
const clientFormAction = async (formData: FormData) => {
+ if (!connectedAddress) {
+ notification.error("Please connect your wallet");
+ return;
+ }
+
try {
- const formState = Object.fromEntries(formData.entries());
- if (formState.title === "" || formState.description === "") {
- notification.error("Title and description are required");
+ const title = formData.get("title");
+ const description = formData.get("description");
+ const askAmount = formData.get("askAmount");
+ if (!title || !description || !askAmount) {
+ notification.error("Please fill all the fields");
return;
}
- const signature = await signMessageAsync({ message: JSON.stringify(formState) });
- const signedMessageObject = {
- signature: signature,
- address: connectedAddress,
- };
+ const signature = await signTypedDataAsync({
+ domain: EIP_712_DOMAIN,
+ types: EIP_712_TYPES__APPLY_FOR_GRANT,
+ primaryType: "Message",
+ message: {
+ title: title as string,
+ description: description as string,
+ askAmount: askAmount as string,
+ },
+ });
- // server action
- const submitGrantActionWithSignedMessage = submitGrantAction.bind(null, signedMessageObject);
- await submitGrantActionWithSignedMessage(formData);
+ const res = await fetch("/api/grants/new", {
+ method: "POST",
+ body: JSON.stringify({ title, description, askAmount, signature, signer: connectedAddress }),
+ });
+
+ if (!res.ok) {
+ const data = await res.json();
+ throw new Error(data.error || "Error submitting grant proposal");
+ }
notification.success("Proposal submitted successfully!");
router.push("/");
@@ -54,6 +72,7 @@ const Form = () => {
placeholder="title"
name="title"
autoComplete="off"
+ type="text"
/>
diff --git a/packages/nextjs/app/submit-grant/_component/SubmitButton.tsx b/packages/nextjs/app/apply/_component/SubmitButton.tsx
similarity index 100%
rename from packages/nextjs/app/submit-grant/_component/SubmitButton.tsx
rename to packages/nextjs/app/apply/_component/SubmitButton.tsx
diff --git a/packages/nextjs/app/submit-grant/page.tsx b/packages/nextjs/app/apply/page.tsx
similarity index 100%
rename from packages/nextjs/app/submit-grant/page.tsx
rename to packages/nextjs/app/apply/page.tsx
diff --git a/packages/nextjs/app/submit-grant/_actions/index.ts b/packages/nextjs/app/submit-grant/_actions/index.ts
deleted file mode 100644
index 434020a..0000000
--- a/packages/nextjs/app/submit-grant/_actions/index.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-"use server";
-
-import { verifyMessage } from "viem";
-import { createGrant } from "~~/services/database/grants";
-import { findUserByAddress } from "~~/services/database/users";
-
-type SignatureAndSigner = {
- signature?: `0x${string}`;
- address?: string;
-};
-
-export const submitGrantAction = async ({ signature, address }: SignatureAndSigner, form: FormData) => {
- try {
- const formData = Object.fromEntries(form.entries());
- if (!formData.title || !formData.description || !formData.askAmount) {
- throw new Error("Invalid form data");
- }
-
- if (!signature || !address) {
- throw new Error("Signature and address are required to submit grant");
- }
-
- const constructedMessage = JSON.stringify(formData);
- const isMessageValid = await verifyMessage({ message: constructedMessage, signature, address });
- if (!isMessageValid) {
- throw new Error("Invalid signature");
- }
-
- // Verif if the builder is present
- const builder = await findUserByAddress(address);
- if (!builder.exists) {
- throw new Error("Only buidlguild builders can submit for grants");
- }
-
- // Save the form data to the database
- const grant = await createGrant({
- title: formData.title as string,
- description: formData.description as string,
- askAmount: Number(formData.askAmount),
- builder: address,
- });
-
- return grant;
- } catch (e) {
- if (e instanceof Error) {
- throw e;
- }
-
- throw new Error("Error processing form");
- }
-};
diff --git a/packages/nextjs/utils/eip712.ts b/packages/nextjs/utils/eip712.ts
index 4a34de6..fdc67ab 100644
--- a/packages/nextjs/utils/eip712.ts
+++ b/packages/nextjs/utils/eip712.ts
@@ -4,6 +4,14 @@ export const EIP_712_DOMAIN = {
chainId: 10,
} as const;
+export const EIP_712_TYPES__APPLY_FOR_GRANT = {
+ Message: [
+ { name: "title", type: "string" },
+ { name: "description", type: "string" },
+ { name: "askAmount", type: "string" },
+ ],
+} as const;
+
// ToDo. We could add more fields (grant title, builder, etc)
export const EIP_712_TYPES__REVIEW_GRANT = {
Message: [