From cad69a5dfae8f966873d8b85d0fd6bd899eeaff6 Mon Sep 17 00:00:00 2001 From: Shiv Bhonde Date: Tue, 18 Feb 2025 15:21:25 +0530 Subject: [PATCH 01/17] configure basic schema --- .../nextjs/services/database/config/schema.ts | 40 ++++++++++++++++++- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/packages/nextjs/services/database/config/schema.ts b/packages/nextjs/services/database/config/schema.ts index 51d4aa97..49f42108 100644 --- a/packages/nextjs/services/database/config/schema.ts +++ b/packages/nextjs/services/database/config/schema.ts @@ -1,8 +1,44 @@ -import { sql } from "drizzle-orm"; -import { pgTable, timestamp, varchar } from "drizzle-orm/pg-core"; +import { relations, sql } from "drizzle-orm"; +import { pgEnum, pgTable, serial, text, timestamp, varchar } from "drizzle-orm/pg-core"; + +export const reviewActionEnum = pgEnum("review_action_enum", ["REJECTED", "ACCEPTED", "SUBMITTED"]); +export const eventTypeEnum = pgEnum("event_type_enum", ["challenge.submit", "challenge.autograde"]); // TODO: Define the right schema. export const users = pgTable("users", { id: varchar("id", { length: 42 }).primaryKey(), creationTimestamp: timestamp("creation_timestamp").default(sql`now()`), }); + +export const userChallenges = pgTable("user_challenges", { + userChallengeId: serial("userChallengeId").primaryKey(), + userAddress: varchar("userAddress", { length: 42 }) + .notNull() + .references(() => users.id), + challengeCode: varchar("challengeCode", { length: 255 }).notNull(), + frontendUrl: varchar("frontendUrl", { length: 255 }), + contractUrl: varchar("contractUrl", { length: 255 }), + reviewComment: text("reviewComment"), // Feedback provided during from the autograder + submittedTimestamp: timestamp("submittedTimestamp").defaultNow(), + reviewAction: reviewActionEnum("reviewAction"), // Final review decision from autograder (REJECTED or ACCEPTED) +}); + +export const usersRelations = relations(users, ({ many }) => ({ + userChallenges: many(userChallenges), + events: many(events), +})); + +export const userChallengesRelations = relations(userChallenges, ({ one }) => ({ + user: one(users, { + fields: [userChallenges.userAddress], + references: [users.id], + }), +})); + +export const events = pgTable("events", { + eventId: serial("eventId").primaryKey(), + eventType: eventTypeEnum("eventType").notNull(), + eventTimestamp: timestamp("eventTimestamp").defaultNow(), + userAddress: varchar("userAddress", { length: 42 }).notNull(), + challengeCode: varchar("challengeCode", { length: 255 }), +}); From 996a3244aa2ada6bae883a3f65bffa5b9ff6a484 Mon Sep 17 00:00:00 2001 From: Shiv Bhonde Date: Tue, 18 Feb 2025 15:34:51 +0530 Subject: [PATCH 02/17] remove usersRelation for events --- packages/nextjs/services/database/config/schema.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/nextjs/services/database/config/schema.ts b/packages/nextjs/services/database/config/schema.ts index 49f42108..fc540e39 100644 --- a/packages/nextjs/services/database/config/schema.ts +++ b/packages/nextjs/services/database/config/schema.ts @@ -25,7 +25,6 @@ export const userChallenges = pgTable("user_challenges", { export const usersRelations = relations(users, ({ many }) => ({ userChallenges: many(userChallenges), - events: many(events), })); export const userChallengesRelations = relations(userChallenges, ({ one }) => ({ From 5455f3674e3fbcaf808e98cafe5d49488aeb5bab Mon Sep 17 00:00:00 2001 From: Shiv Bhonde Date: Tue, 18 Feb 2025 15:49:15 +0530 Subject: [PATCH 03/17] basi submission flow --- .../challenges/[challengeId]/submit/route.ts | 90 +++++++++++++ .../_components/SubmitChallengeButton.tsx | 29 ++++ .../_components/SubmitChallengeModal.tsx | 127 ++++++++++++++++++ .../app/challenge/[challengeId]/page.tsx | 2 + .../nextjs/services/api/challenges/index.ts | 28 ++++ .../nextjs/services/database/config/schema.ts | 32 +++-- .../services/database/repositories/events.ts | 9 ++ .../database/repositories/userChallenges.ts | 21 +++ packages/nextjs/utils/eip712/challenge.ts | 45 +++++++ 9 files changed, 370 insertions(+), 13 deletions(-) create mode 100644 packages/nextjs/app/api/challenges/[challengeId]/submit/route.ts create mode 100644 packages/nextjs/app/challenge/[challengeId]/_components/SubmitChallengeButton.tsx create mode 100644 packages/nextjs/app/challenge/[challengeId]/_components/SubmitChallengeModal.tsx create mode 100644 packages/nextjs/services/api/challenges/index.ts create mode 100644 packages/nextjs/services/database/repositories/events.ts create mode 100644 packages/nextjs/services/database/repositories/userChallenges.ts create mode 100644 packages/nextjs/utils/eip712/challenge.ts diff --git a/packages/nextjs/app/api/challenges/[challengeId]/submit/route.ts b/packages/nextjs/app/api/challenges/[challengeId]/submit/route.ts new file mode 100644 index 00000000..de1b2bc3 --- /dev/null +++ b/packages/nextjs/app/api/challenges/[challengeId]/submit/route.ts @@ -0,0 +1,90 @@ +import { NextRequest, NextResponse } from "next/server"; +import { createEvent } from "~~/services/database/repositories/events"; +import { upsertUserChallenge } from "~~/services/database/repositories/userChallenges"; +import { findUserByAddress } from "~~/services/database/repositories/users"; +import { isValidEIP712ChallengeSubmitSignature } from "~~/utils/eip712/challenge"; + +export type ChallengeSubmitPayload = { + userAddress: string; + frontendUrl: string; + contractUrl: string; + signature: `0x${string}`; +}; + +export type AutogradingResult = { + success: boolean; + feedback: string; +}; + +// TODO: Remove this and make request to actual autograder +async function mockAutograding(contractUrl: string): Promise { + console.log("Mock autograding for contract:", contractUrl); + await new Promise(resolve => setTimeout(resolve, 1000)); + return { + success: false, + feedback: "You Failed", + }; +} + +export async function POST(req: NextRequest, { params }: { params: { challengeId: string } }) { + try { + console.log("Submit challenge"); + const challengeId = params.challengeId; + const { userAddress, frontendUrl, contractUrl, signature } = (await req.json()) as ChallengeSubmitPayload; + const lowerCasedUserAddress = userAddress.toLowerCase(); + + if (!userAddress || !frontendUrl || !contractUrl || !signature) { + return NextResponse.json({ error: "Missing required fields" }, { status: 400 }); + } + + const isValidSignature = await isValidEIP712ChallengeSubmitSignature({ + address: userAddress, + signature, + challengeId, + frontendUrl, + contractUrl, + }); + + if (!isValidSignature) { + return NextResponse.json({ error: "Invalid signature" }, { status: 401 }); + } + + const user = await findUserByAddress(lowerCasedUserAddress); + if (user.length === 0) { + return NextResponse.json({ error: "User not found" }, { status: 404 }); + } + + await createEvent({ + eventType: "challenge.submit", + userAddress: lowerCasedUserAddress, + challengeCode: challengeId, + }); + + // TODO: Make request to actual autograder + const gradingResult = await mockAutograding(contractUrl); + + await upsertUserChallenge({ + userAddress: lowerCasedUserAddress, + challengeCode: challengeId, + frontendUrl, + contractUrl, + reviewAction: gradingResult.success ? "ACCEPTED" : "REJECTED", + reviewComment: gradingResult.feedback, + }); + + await createEvent({ + eventType: "challenge.autograde", + userAddress: lowerCasedUserAddress, + challengeCode: challengeId, + }); + + return NextResponse.json({ + success: true, + message: "Challenge submitted and graded successfully", + autoGradingResult: gradingResult, + }); + } catch (error) { + console.error("Error submitting challenge:", error); + return NextResponse.json({ error: "Internal Server Error" }, { status: 500 }); + } +} diff --git a/packages/nextjs/app/challenge/[challengeId]/_components/SubmitChallengeButton.tsx b/packages/nextjs/app/challenge/[challengeId]/_components/SubmitChallengeButton.tsx new file mode 100644 index 00000000..f8b5120f --- /dev/null +++ b/packages/nextjs/app/challenge/[challengeId]/_components/SubmitChallengeButton.tsx @@ -0,0 +1,29 @@ +"use client"; + +import { useRef } from "react"; +import { SubmitChallengeModal } from "./SubmitChallengeModal"; +import { useAccount } from "wagmi"; +import { useUser } from "~~/hooks/useUser"; + +export const SubmitChallengeButton = ({ challengeId }: { challengeId: string }) => { + const submitChallengeModalRef = useRef(null); + const { address: connectedAddress } = useAccount(); + + const { data: user, isLoading: isLoadingUser } = useUser(connectedAddress); + return ( + <> + + submitChallengeModalRef.current?.close()} + /> + + ); +}; diff --git a/packages/nextjs/app/challenge/[challengeId]/_components/SubmitChallengeModal.tsx b/packages/nextjs/app/challenge/[challengeId]/_components/SubmitChallengeModal.tsx new file mode 100644 index 00000000..3c3a8c82 --- /dev/null +++ b/packages/nextjs/app/challenge/[challengeId]/_components/SubmitChallengeModal.tsx @@ -0,0 +1,127 @@ +import { forwardRef, useState } from "react"; +import { useMutation } from "@tanstack/react-query"; +import { useAccount, useSignTypedData } from "wagmi"; +import { QuestionMarkCircleIcon } from "@heroicons/react/24/outline"; +import { InputBase } from "~~/components/scaffold-eth"; +import { submitChallenge } from "~~/services/api/challenges"; +import { EIP_712_TYPED_DATA__CHALLENGE_SUBMIT } from "~~/utils/eip712/challenge"; +import { notification } from "~~/utils/scaffold-eth"; + +type SubmitChallengeModalProps = { + challengeId: string; + closeModal: () => void; +}; + +export const SubmitChallengeModal = forwardRef( + ({ closeModal, challengeId }, ref) => { + const [frontendUrl, setFrontendUrl] = useState(""); + const [etherscanUrl, setEtherscanUrl] = useState(""); + + const { address } = useAccount(); + const { signTypedDataAsync } = useSignTypedData(); + + const { mutate: submit, isPending } = useMutation({ + mutationFn: async () => { + if (!address) throw new Error("Wallet not connected"); + + const message = { + ...EIP_712_TYPED_DATA__CHALLENGE_SUBMIT.message, + challengeId, + frontendUrl, + contractUrl: etherscanUrl, + }; + + const signature = await signTypedDataAsync({ + ...EIP_712_TYPED_DATA__CHALLENGE_SUBMIT, + message, + }); + + return submitChallenge({ + challengeId, + userAddress: address, + frontendUrl, + contractUrl: etherscanUrl, + signature, + }); + }, + onSuccess: () => { + notification.success("Challenge submitted successfully!"); + closeModal(); + }, + onError: (error: Error) => { + notification.error(error.message); + }, + }); + + return ( + +
+
+
+

Submit Challenge

+
+ +
+ +

{challengeId}

+ +
+
+
+ Deployed URL +
+ +
+
+ setFrontendUrl(e)} + /> +
+ +
+
+ Etherscan URL +
+ +
+
+ { + setEtherscanUrl(e); + }} + /> +
+ +
+ +
+
+
+
+ +
+
+ ); + }, +); + +SubmitChallengeModal.displayName = "SubmitChallengeModal"; diff --git a/packages/nextjs/app/challenge/[challengeId]/page.tsx b/packages/nextjs/app/challenge/[challengeId]/page.tsx index 78120d7e..85965fa0 100644 --- a/packages/nextjs/app/challenge/[challengeId]/page.tsx +++ b/packages/nextjs/app/challenge/[challengeId]/page.tsx @@ -1,3 +1,4 @@ +import { SubmitChallengeButton } from "./_components/SubmitChallengeButton"; import { MDXRemote } from "next-mdx-remote/rsc"; import { findChallengeById, getAllChallenges } from "~~/services/database/repositories/challenges"; import { fetchGithubReadme } from "~~/services/github"; @@ -32,6 +33,7 @@ export default async function ChallengePage({ params }: { params: { challengeId: ) : (
Failed to load challenge content
)} + ); } diff --git a/packages/nextjs/services/api/challenges/index.ts b/packages/nextjs/services/api/challenges/index.ts new file mode 100644 index 00000000..7bd41944 --- /dev/null +++ b/packages/nextjs/services/api/challenges/index.ts @@ -0,0 +1,28 @@ +import { ChallengeSubmitPayload } from "~~/app/api/challenges/[challengeId]/submit/route"; + +export const submitChallenge = async ({ + challengeId, + userAddress, + frontendUrl, + contractUrl, + signature, +}: ChallengeSubmitPayload & { challengeId: string }) => { + const response = await fetch(`/api/challenges/${challengeId}/submit`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + userAddress, + frontendUrl, + contractUrl, + signature, + }), + }); + + if (!response.ok) { + throw new Error(`Failed to submit challenge: ${response.status} ${response.statusText}`); + } + + return response.json(); +}; diff --git a/packages/nextjs/services/database/config/schema.ts b/packages/nextjs/services/database/config/schema.ts index fc540e39..a0024722 100644 --- a/packages/nextjs/services/database/config/schema.ts +++ b/packages/nextjs/services/database/config/schema.ts @@ -1,5 +1,5 @@ import { relations, sql } from "drizzle-orm"; -import { pgEnum, pgTable, serial, text, timestamp, varchar } from "drizzle-orm/pg-core"; +import { pgEnum, pgTable, serial, text, timestamp, unique, varchar } from "drizzle-orm/pg-core"; export const reviewActionEnum = pgEnum("review_action_enum", ["REJECTED", "ACCEPTED", "SUBMITTED"]); export const eventTypeEnum = pgEnum("event_type_enum", ["challenge.submit", "challenge.autograde"]); @@ -10,18 +10,24 @@ export const users = pgTable("users", { creationTimestamp: timestamp("creation_timestamp").default(sql`now()`), }); -export const userChallenges = pgTable("user_challenges", { - userChallengeId: serial("userChallengeId").primaryKey(), - userAddress: varchar("userAddress", { length: 42 }) - .notNull() - .references(() => users.id), - challengeCode: varchar("challengeCode", { length: 255 }).notNull(), - frontendUrl: varchar("frontendUrl", { length: 255 }), - contractUrl: varchar("contractUrl", { length: 255 }), - reviewComment: text("reviewComment"), // Feedback provided during from the autograder - submittedTimestamp: timestamp("submittedTimestamp").defaultNow(), - reviewAction: reviewActionEnum("reviewAction"), // Final review decision from autograder (REJECTED or ACCEPTED) -}); +export const userChallenges = pgTable( + "user_challenges", + { + userChallengeId: serial("userChallengeId").primaryKey(), + userAddress: varchar("userAddress", { length: 42 }) + .notNull() + .references(() => users.id), + challengeCode: varchar("challengeCode", { length: 255 }).notNull(), + frontendUrl: varchar("frontendUrl", { length: 255 }), + contractUrl: varchar("contractUrl", { length: 255 }), + reviewComment: text("reviewComment"), + submittedTimestamp: timestamp("submittedTimestamp").defaultNow(), + reviewAction: reviewActionEnum("reviewAction"), + }, + table => ({ + uniqueUserChallenge: unique().on(table.userAddress, table.challengeCode), + }), +); export const usersRelations = relations(users, ({ many }) => ({ userChallenges: many(userChallenges), diff --git a/packages/nextjs/services/database/repositories/events.ts b/packages/nextjs/services/database/repositories/events.ts new file mode 100644 index 00000000..8737ee94 --- /dev/null +++ b/packages/nextjs/services/database/repositories/events.ts @@ -0,0 +1,9 @@ +import { InferInsertModel } from "drizzle-orm"; +import { db } from "~~/services/database/config/postgresClient"; +import { events } from "~~/services/database/config/schema"; + +export type EventInsert = InferInsertModel; + +export async function createEvent(event: EventInsert) { + return await db.insert(events).values(event).returning(); +} diff --git a/packages/nextjs/services/database/repositories/userChallenges.ts b/packages/nextjs/services/database/repositories/userChallenges.ts new file mode 100644 index 00000000..8bac855f --- /dev/null +++ b/packages/nextjs/services/database/repositories/userChallenges.ts @@ -0,0 +1,21 @@ +import { InferInsertModel } from "drizzle-orm"; +import { db } from "~~/services/database/config/postgresClient"; +import { userChallenges } from "~~/services/database/config/schema"; + +export type UserChallengeInsert = InferInsertModel; + +export async function upsertUserChallenge(challenge: UserChallengeInsert) { + return await db + .insert(userChallenges) + .values(challenge) + .onConflictDoUpdate({ + target: [userChallenges.userAddress, userChallenges.challengeCode], + set: { + frontendUrl: challenge.frontendUrl, + contractUrl: challenge.contractUrl, + reviewAction: challenge.reviewAction, + reviewComment: challenge.reviewComment, + submittedTimestamp: challenge.submittedTimestamp, + }, + }); +} diff --git a/packages/nextjs/utils/eip712/challenge.ts b/packages/nextjs/utils/eip712/challenge.ts new file mode 100644 index 00000000..9b865f7c --- /dev/null +++ b/packages/nextjs/utils/eip712/challenge.ts @@ -0,0 +1,45 @@ +import { EIP_712_DOMAIN, isValidEip712Signature } from "./common"; + +export const EIP_712_TYPED_DATA__CHALLENGE_SUBMIT = { + domain: EIP_712_DOMAIN, + types: { + Message: [ + { name: "action", type: "string" }, + { name: "challengeId", type: "string" }, + { name: "frontendUrl", type: "string" }, + { name: "contractUrl", type: "string" }, + ], + }, + primaryType: "Message", + message: { + // TODO: Maybe have beter message? + action: "Submit Challenge", + }, +} as const; + +export const isValidEIP712ChallengeSubmitSignature = async ({ + address, + signature, + challengeId, + frontendUrl, + contractUrl, +}: { + address: string; + signature: `0x${string}`; + challengeId: string; + frontendUrl: string; + contractUrl: string; +}) => { + const typedData = { + ...EIP_712_TYPED_DATA__CHALLENGE_SUBMIT, + message: { + ...EIP_712_TYPED_DATA__CHALLENGE_SUBMIT.message, + challengeId, + frontendUrl, + contractUrl, + }, + signature, + }; + + return await isValidEip712Signature({ typedData, address }); +}; From a6d6c8020294fa91e1c37134aad552f8fb04c1fc Mon Sep 17 00:00:00 2001 From: Shiv Bhonde Date: Tue, 18 Feb 2025 15:52:57 +0530 Subject: [PATCH 04/17] make autograder result true --- .../nextjs/app/api/challenges/[challengeId]/submit/route.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/nextjs/app/api/challenges/[challengeId]/submit/route.ts b/packages/nextjs/app/api/challenges/[challengeId]/submit/route.ts index de1b2bc3..41ea2fcc 100644 --- a/packages/nextjs/app/api/challenges/[challengeId]/submit/route.ts +++ b/packages/nextjs/app/api/challenges/[challengeId]/submit/route.ts @@ -21,8 +21,8 @@ async function mockAutograding(contractUrl: string): Promise console.log("Mock autograding for contract:", contractUrl); await new Promise(resolve => setTimeout(resolve, 1000)); return { - success: false, - feedback: "You Failed", + success: true, + feedback: "All tests passed successfully! Great work!", }; } From 94b23fbb3c4c1ffb021e6ab424a06daea17b577f Mon Sep 17 00:00:00 2001 From: Shiv Bhonde Date: Tue, 18 Feb 2025 17:01:21 +0530 Subject: [PATCH 05/17] basic builders address page --- .../nextjs/app/builders/[address]/page.tsx | 75 +++++++++++++++++++ .../database/repositories/userChallenges.ts | 6 +- 2 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 packages/nextjs/app/builders/[address]/page.tsx diff --git a/packages/nextjs/app/builders/[address]/page.tsx b/packages/nextjs/app/builders/[address]/page.tsx new file mode 100644 index 00000000..59bde305 --- /dev/null +++ b/packages/nextjs/app/builders/[address]/page.tsx @@ -0,0 +1,75 @@ +import { QuestionMarkCircleIcon } from "@heroicons/react/24/outline"; +import { findUserChallengesByAddress } from "~~/services/database/repositories/userChallenges"; + +export default async function BuilderPage({ params }: { params: { address: string } }) { + const { address: userAddress } = params; + const challenges = await findUserChallengesByAddress(userAddress); + + return ( +
+
+

Challenges

+
+
+ + + + + + + + + + + + + {challenges.map(challenge => ( + + + + + + + + + ))} + +
NAMECONTRACTLIVE DEMOSUBMITED ATSTATUS
🏃‍♂️ Challenge {challenge.challengeCode} + {challenge.contractUrl ? ( + + Code + + ) : ( + "-" + )} + + {challenge.frontendUrl ? ( + + Demo + + ) : ( + "-" + )} + {challenge.submittedTimestamp ? challenge.submittedTimestamp.toLocaleString() : "-"} + + {challenge.reviewAction?.toLowerCase() || "pending"} + + + {challenge.reviewComment && ( +
+ +
+ )} +
+
+
+ ); +} diff --git a/packages/nextjs/services/database/repositories/userChallenges.ts b/packages/nextjs/services/database/repositories/userChallenges.ts index 8bac855f..93057282 100644 --- a/packages/nextjs/services/database/repositories/userChallenges.ts +++ b/packages/nextjs/services/database/repositories/userChallenges.ts @@ -1,9 +1,13 @@ -import { InferInsertModel } from "drizzle-orm"; +import { InferInsertModel, eq } from "drizzle-orm"; import { db } from "~~/services/database/config/postgresClient"; import { userChallenges } from "~~/services/database/config/schema"; export type UserChallengeInsert = InferInsertModel; +export async function findUserChallengesByAddress(userAddress: string) { + return await db.select().from(userChallenges).where(eq(userChallenges.userAddress, userAddress.toLowerCase())); +} + export async function upsertUserChallenge(challenge: UserChallengeInsert) { return await db .insert(userChallenges) From 1fa14905983c83203cc4993f33d09a49a8e3a844 Mon Sep 17 00:00:00 2001 From: Shiv Bhonde Date: Tue, 18 Feb 2025 17:05:26 +0530 Subject: [PATCH 06/17] push to builderProfile --- .../[challengeId]/_components/SubmitChallengeModal.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/nextjs/app/challenge/[challengeId]/_components/SubmitChallengeModal.tsx b/packages/nextjs/app/challenge/[challengeId]/_components/SubmitChallengeModal.tsx index 3c3a8c82..d5452857 100644 --- a/packages/nextjs/app/challenge/[challengeId]/_components/SubmitChallengeModal.tsx +++ b/packages/nextjs/app/challenge/[challengeId]/_components/SubmitChallengeModal.tsx @@ -1,4 +1,5 @@ import { forwardRef, useState } from "react"; +import { useRouter } from "next/navigation"; import { useMutation } from "@tanstack/react-query"; import { useAccount, useSignTypedData } from "wagmi"; import { QuestionMarkCircleIcon } from "@heroicons/react/24/outline"; @@ -12,10 +13,12 @@ type SubmitChallengeModalProps = { closeModal: () => void; }; +// TODO: Add input validation for URLs export const SubmitChallengeModal = forwardRef( ({ closeModal, challengeId }, ref) => { const [frontendUrl, setFrontendUrl] = useState(""); const [etherscanUrl, setEtherscanUrl] = useState(""); + const router = useRouter(); const { address } = useAccount(); const { signTypedDataAsync } = useSignTypedData(); @@ -46,6 +49,7 @@ export const SubmitChallengeModal = forwardRef { notification.success("Challenge submitted successfully!"); + router.push(`/builders/${address}`); closeModal(); }, onError: (error: Error) => { From 529645141aa4e2025796a43ee3e8f3deabf64af2 Mon Sep 17 00:00:00 2001 From: Shiv Bhonde Date: Tue, 18 Feb 2025 17:15:08 +0530 Subject: [PATCH 07/17] add basic validation --- .../_components/SubmitChallengeModal.tsx | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/nextjs/app/challenge/[challengeId]/_components/SubmitChallengeModal.tsx b/packages/nextjs/app/challenge/[challengeId]/_components/SubmitChallengeModal.tsx index d5452857..fb3a404c 100644 --- a/packages/nextjs/app/challenge/[challengeId]/_components/SubmitChallengeModal.tsx +++ b/packages/nextjs/app/challenge/[challengeId]/_components/SubmitChallengeModal.tsx @@ -8,12 +8,13 @@ import { submitChallenge } from "~~/services/api/challenges"; import { EIP_712_TYPED_DATA__CHALLENGE_SUBMIT } from "~~/utils/eip712/challenge"; import { notification } from "~~/utils/scaffold-eth"; +const VALID_BLOCK_EXPLORER_HOSTS = ["sepolia.etherscan.io", "sepolia-optimism.etherscan.io"]; + type SubmitChallengeModalProps = { challengeId: string; closeModal: () => void; }; -// TODO: Add input validation for URLs export const SubmitChallengeModal = forwardRef( ({ closeModal, challengeId }, ref) => { const [frontendUrl, setFrontendUrl] = useState(""); @@ -26,6 +27,16 @@ export const SubmitChallengeModal = forwardRef { if (!address) throw new Error("Wallet not connected"); + try { + new URL(frontendUrl); + const etherscan = new URL(etherscanUrl); + + if (!VALID_BLOCK_EXPLORER_HOSTS.includes(etherscan.host)) { + throw new Error("Please use a valid Etherscan testnet URL (Sepolia)"); + } + } catch (e) { + throw new Error("Please enter valid URLs"); + } const message = { ...EIP_712_TYPED_DATA__CHALLENGE_SUBMIT.message, From 566bec0d43639b42dee9ffed02f633f6948e0567 Mon Sep 17 00:00:00 2001 From: Shiv Bhonde Date: Tue, 18 Feb 2025 17:19:44 +0530 Subject: [PATCH 08/17] make the submit button float in challenge details page --- .../[challengeId]/_components/SubmitChallengeButton.tsx | 2 +- packages/nextjs/app/challenge/[challengeId]/page.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/nextjs/app/challenge/[challengeId]/_components/SubmitChallengeButton.tsx b/packages/nextjs/app/challenge/[challengeId]/_components/SubmitChallengeButton.tsx index f8b5120f..d432d51d 100644 --- a/packages/nextjs/app/challenge/[challengeId]/_components/SubmitChallengeButton.tsx +++ b/packages/nextjs/app/challenge/[challengeId]/_components/SubmitChallengeButton.tsx @@ -13,7 +13,7 @@ export const SubmitChallengeButton = ({ challengeId }: { challengeId: string }) return ( <>