diff --git a/packages/nextjs/app/api/submissions/route.ts b/packages/nextjs/app/api/submissions/route.ts
index 20d91d4..7672e06 100644
--- a/packages/nextjs/app/api/submissions/route.ts
+++ b/packages/nextjs/app/api/submissions/route.ts
@@ -1,5 +1,9 @@
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 { SubmissionInsert } from "~~/services/database/repositories/submissions";
+import { EIP_712_DOMAIN, EIP_712_TYPES__SUBMISSION } from "~~/utils/eip712";
export async function GET() {
try {
@@ -10,3 +14,53 @@ export async function GET() {
return NextResponse.json({ error: "Error fetching submissions" }, { status: 500 });
}
}
+
+export type CreateNewSubmissionBody = SubmissionInsert & { signature: `0x${string}`; signer: string };
+
+export async function POST(req: Request) {
+ try {
+ const { title, description, linkToRepository, signature, signer } = (await req.json()) as CreateNewSubmissionBody;
+
+ 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..e280f0a
--- /dev/null
+++ b/packages/nextjs/app/submit/_component/Form.tsx
@@ -0,0 +1,116 @@
+"use client";
+
+import React, { useState } from "react";
+import { useRouter } from "next/navigation";
+import SubmitButton from "./SubmitButton";
+import { useMutation } from "@tanstack/react-query";
+import { useAccount, useSignTypedData } from "wagmi";
+import { CreateNewSubmissionBody } from "~~/app/api/submissions/route";
+import { EIP_712_DOMAIN, EIP_712_TYPES__SUBMISSION } from "~~/utils/eip712";
+import { postMutationFetcher } from "~~/utils/react-query";
+import { notification } from "~~/utils/scaffold-eth";
+
+const MAX_DESCRIPTION_LENGTH = 750;
+
+const Form = () => {
+ const { address: connectedAddress } = useAccount();
+ const [descriptionLength, setDescriptionLength] = useState(0);
+ const { signTypedDataAsync } = useSignTypedData();
+ const router = useRouter();
+ const { mutateAsync: postNewSubmission } = useMutation({
+ mutationFn: (newSubmission: CreateNewSubmissionBody) =>
+ postMutationFetcher("/api/submissions", { body: newSubmission }),
+ });
+
+ 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 postNewSubmission({ title, description, linkToRepository, signature, signer: connectedAddress });
+
+ notification.success("Extension submitted successfully!");
+ router.push("/");
+ } catch (error: any) {
+ if (error instanceof Error) {
+ notification.error(error.message);
+ return;
+ }
+ notification.error("Something went wrong");
+ }
+ };
+
+ return (
+
+ );
+};
+
+export default Form;
diff --git a/packages/nextjs/app/submit/_component/SubmitButton.tsx b/packages/nextjs/app/submit/_component/SubmitButton.tsx
new file mode 100644
index 0000000..89aca5a
--- /dev/null
+++ b/packages/nextjs/app/submit/_component/SubmitButton.tsx
@@ -0,0 +1,28 @@
+"use client";
+
+import { useFormStatus } from "react-dom";
+import { useAccount } from "wagmi";
+import { RainbowKitCustomConnectButton } from "~~/components/scaffold-eth";
+
+// To use useFormStatus we need to make sure button is child of form
+const SubmitButton = () => {
+ const { pending } = useFormStatus();
+ const { isConnected } = useAccount();
+
+ return (
+
+ {isConnected ? (
+
+ ) : (
+
+ )}
+
+ );
+};
+
+export default SubmitButton;
diff --git a/packages/nextjs/app/submit/page.tsx b/packages/nextjs/app/submit/page.tsx
new file mode 100644
index 0000000..d2806ba
--- /dev/null
+++ b/packages/nextjs/app/submit/page.tsx
@@ -0,0 +1,14 @@
+import Form from "./_component/Form";
+import { NextPage } from "next";
+
+const Submit: NextPage = () => {
+ return (
+
+
Submit Extension
+
Submit your SE-2 extension.
+
+
+ );
+};
+
+export default Submit;
diff --git a/packages/nextjs/components/Header.tsx b/packages/nextjs/components/Header.tsx
index f24a1de..5b0355b 100644
--- a/packages/nextjs/components/Header.tsx
+++ b/packages/nextjs/components/Header.tsx
@@ -19,6 +19,10 @@ export const menuLinks: HeaderMenuLink[] = [
label: "Home",
href: "/",
},
+ {
+ label: "Submit",
+ href: "/submit",
+ },
{
label: "Debug Contracts",
href: "/debug",
diff --git a/packages/nextjs/components/scaffold-eth/RainbowKitCustomConnectButton/index.tsx b/packages/nextjs/components/scaffold-eth/RainbowKitCustomConnectButton/index.tsx
index 6521200..aec40a9 100644
--- a/packages/nextjs/components/scaffold-eth/RainbowKitCustomConnectButton/index.tsx
+++ b/packages/nextjs/components/scaffold-eth/RainbowKitCustomConnectButton/index.tsx
@@ -14,7 +14,7 @@ import { getBlockExplorerAddressLink } from "~~/utils/scaffold-eth";
/**
* Custom Wagmi Connect Button (watch balance + custom design)
*/
-export const RainbowKitCustomConnectButton = () => {
+export const RainbowKitCustomConnectButton = ({ fullWidth }: { fullWidth?: boolean }) => {
const networkColor = useNetworkColor();
const { targetNetwork } = useTargetNetwork();
@@ -31,7 +31,11 @@ export const RainbowKitCustomConnectButton = () => {
{(() => {
if (!connected) {
return (
-