From ffdc041156b82e38f032cbc2807b563056048100 Mon Sep 17 00:00:00 2001 From: Damian Date: Wed, 31 Jul 2024 14:53:02 -0300 Subject: [PATCH 1/7] First draft working with swr --- packages/nextjs/app/api/submissions/route.ts | 61 ++++++++- .../nextjs/app/submit/_component/Form.tsx | 121 ++++++++++++++++++ .../app/submit/_component/SubmitButton.tsx | 24 ++++ packages/nextjs/app/submit/page.tsx | 14 ++ packages/nextjs/package.json | 2 + .../nextjs/services/database/config/schema.ts | 1 + .../database/repositories/builders.ts | 17 +++ packages/nextjs/services/database/seed.ts | 2 + packages/nextjs/utils/eip712.ts | 55 ++++++++ packages/nextjs/utils/swr.ts | 30 +++++ yarn.lock | 34 ++++- 11 files changed, 359 insertions(+), 2 deletions(-) create mode 100644 packages/nextjs/app/submit/_component/Form.tsx create mode 100644 packages/nextjs/app/submit/_component/SubmitButton.tsx create mode 100644 packages/nextjs/app/submit/page.tsx create mode 100644 packages/nextjs/services/database/repositories/builders.ts create mode 100644 packages/nextjs/utils/eip712.ts create mode 100644 packages/nextjs/utils/swr.ts diff --git a/packages/nextjs/app/api/submissions/route.ts b/packages/nextjs/app/api/submissions/route.ts index 20d91d4..f2ab5c5 100644 --- a/packages/nextjs/app/api/submissions/route.ts +++ b/packages/nextjs/app/api/submissions/route.ts @@ -1,5 +1,8 @@ import { NextResponse } from "next/server"; -import { getAllSubmissions } from "~~/services/database/repositories/submissions"; +import { recoverTypedDataAddress } from "viem"; +import { createBuilder, getBuilderById } from "~~/services/database/repositories/builders"; +import { createSubmission, getAllSubmissions } from "~~/services/database/repositories/submissions"; +import { EIP_712_DOMAIN, EIP_712_TYPES__SUBMISSION } from "~~/utils/eip712"; export async function GET() { try { @@ -10,3 +13,59 @@ export async function GET() { return NextResponse.json({ error: "Error fetching submissions" }, { status: 500 }); } } + +type ReqBody = { + title?: string; + description?: string; + linkToRepository?: string; + signature?: `0x${string}`; + signer?: string; +}; + +export async function POST(req: Request) { + try { + const { title, description, linkToRepository, signature, signer } = (await req.json()) as ReqBody; + + if ( + !title || + !description || + !linkToRepository || + !signature || + !signer || + description.length > 750 || + title.length > 75 + ) { + return NextResponse.json({ error: "Invalid form details submitted" }, { status: 400 }); + } + + const recoveredAddress = await recoverTypedDataAddress({ + domain: EIP_712_DOMAIN, + types: EIP_712_TYPES__SUBMISSION, + primaryType: "Message", + message: { title, description, linkToRepository }, + signature: signature, + }); + + if (recoveredAddress !== signer) { + return NextResponse.json({ error: "Recovered address did not match signer" }, { status: 401 }); + } + + const builder = await getBuilderById(signer); + + if (!builder) { + await createBuilder({ id: signer, role: "user" }); + } + + const submission = await createSubmission({ + title: title, + description: description, + linkToRepository: linkToRepository, + builder: signer, + }); + + return NextResponse.json({ submission }, { status: 201 }); + } catch (e) { + console.error(e); + return NextResponse.json({ error: "Error processing form" }, { status: 500 }); + } +} diff --git a/packages/nextjs/app/submit/_component/Form.tsx b/packages/nextjs/app/submit/_component/Form.tsx new file mode 100644 index 0000000..e77e021 --- /dev/null +++ b/packages/nextjs/app/submit/_component/Form.tsx @@ -0,0 +1,121 @@ +"use client"; + +import React, { useState } from "react"; +import { useRouter } from "next/navigation"; +import SubmitButton from "./SubmitButton"; +import useSWRMutation from "swr/mutation"; +import { useAccount, useSignTypedData } from "wagmi"; +import { EIP_712_DOMAIN, EIP_712_TYPES__SUBMISSION } from "~~/utils/eip712"; +import { notification } from "~~/utils/scaffold-eth"; +import { postMutationFetcher } from "~~/utils/swr"; + +// TODO: move to a shared location +type ReqBody = { + title?: string; + description?: string; + linkToRepository?: string; + signature?: `0x${string}`; + signer?: string; +}; + +const MAX_DESCRIPTION_LENGTH = 750; + +const Form = () => { + const { address: connectedAddress } = useAccount(); + const [descriptionLength, setDescriptionLength] = useState(0); + const { signTypedDataAsync } = useSignTypedData(); + const router = useRouter(); + const { trigger: postNewGrant } = useSWRMutation("/api/submissions", postMutationFetcher); + + const clientFormAction = async (formData: FormData) => { + if (!connectedAddress) { + notification.error("Please connect your wallet"); + return; + } + + try { + const title = formData.get("title") as string; + const description = formData.get("description") as string; + const linkToRepository = formData.get("linkToRepository") as string; + if (!title || !description || !linkToRepository) { + notification.error("Please fill all the fields"); + return; + } + + const signature = await signTypedDataAsync({ + domain: EIP_712_DOMAIN, + types: EIP_712_TYPES__SUBMISSION, + primaryType: "Message", + message: { + title: title, + description: description, + linkToRepository: linkToRepository, + }, + }); + + await postNewGrant({ title, description, linkToRepository, signature, signer: connectedAddress }); + + notification.success("Proposal submitted successfully!"); + router.push("/"); + } catch (error: any) { + if (error instanceof Error) { + notification.error(error.message); + return; + } + notification.error("Something went wrong"); + } + }; + + return ( +
+
+

Submit Proposal

+
+

Title

+
+ +
+
+
+

Description

+
+