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: [