From 597487bf0f99a97c89bc20e0082ae4ec8a0d6e45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20S=C3=A1nchez?= Date: Tue, 24 Dec 2024 12:08:59 +0100 Subject: [PATCH] Offchain signature => register user --- packages/nextjs/app/api/register/route.ts | 53 +++++++++++++++ packages/nextjs/app/page.tsx | 66 ++++++++++++++++--- .../services/database/repositories/users.ts | 5 +- packages/nextjs/utils/eip712.ts | 11 ++++ 4 files changed, 124 insertions(+), 11 deletions(-) create mode 100644 packages/nextjs/app/api/register/route.ts create mode 100644 packages/nextjs/utils/eip712.ts diff --git a/packages/nextjs/app/api/register/route.ts b/packages/nextjs/app/api/register/route.ts new file mode 100644 index 0000000..60eaf37 --- /dev/null +++ b/packages/nextjs/app/api/register/route.ts @@ -0,0 +1,53 @@ +import { NextResponse } from "next/server"; +import { recoverTypedDataAddress } from "viem"; +import { createUser, findUserByAddress } from "~~/services/database/repositories/users"; +import { EIP_712_DOMAIN, EIP_712_TYPES__REGISTER } from "~~/utils/eip712"; + +type RegisterPayload = { + signerAddress?: string; + signature?: `0x${string}`; +}; + +export async function POST(req: Request) { + try { + const { signerAddress, signature } = (await req.json()) as RegisterPayload; + + if (!signerAddress || !signature) { + return new Response("Missing signer or signature", { status: 400 }); + } + + const signerData = await findUserByAddress(signerAddress); + if (signerData.length > 0) { + console.error("Unauthorized", signerAddress); + return NextResponse.json({ error: "User already registered" }, { status: 401 }); + } + + let isValidSignature = false; + + const typedData = { + domain: EIP_712_DOMAIN, + types: EIP_712_TYPES__REGISTER, + primaryType: "Message", + message: { + action: "Register", + description: "I want to register my account into Start Ethereum signing this offchain message", + }, + signature, + } as const; + + const recoveredAddress = await recoverTypedDataAddress(typedData); + isValidSignature = recoveredAddress === signerAddress; + + if (!isValidSignature) { + console.error("Signer and Recovered address does not match"); + return NextResponse.json({ error: "Unauthorized in batch" }, { status: 401 }); + } + + await createUser({ id: signerAddress }); + + return NextResponse.json({ message: "User registered successfully" }, { status: 200 }); + } catch (error) { + console.error("Error when registering", error); + return NextResponse.json({ error: "Internal Server Error" }, { status: 500 }); + } +} diff --git a/packages/nextjs/app/page.tsx b/packages/nextjs/app/page.tsx index 4d79eca..b8d3aaa 100644 --- a/packages/nextjs/app/page.tsx +++ b/packages/nextjs/app/page.tsx @@ -1,16 +1,51 @@ "use client"; import { useState } from "react"; +import Link from "next/link"; import type { NextPage } from "next"; -import { useAccount } from "wagmi"; +import { useAccount, useSignTypedData } from "wagmi"; import { CheckCircleIcon } from "@heroicons/react/24/solid"; import { Address, RainbowKitCustomConnectButton } from "~~/components/scaffold-eth"; +import { EIP_712_DOMAIN, EIP_712_TYPES__REGISTER } from "~~/utils/eip712"; const Home: NextPage = () => { const { isConnected, address } = useAccount(); const [hasWalletInstalled, setHasWalletInstalled] = useState(false); const [hasSignedMessage, setHasSignedMessage] = useState(false); + const { signTypedDataAsync, isPending: isSigningMessage } = useSignTypedData(); + + const handleSignMessage = async () => { + try { + const signature = await signTypedDataAsync({ + domain: EIP_712_DOMAIN, + types: EIP_712_TYPES__REGISTER, + primaryType: "Message", + message: { + action: "Register", + description: "I want to register my account into Start Ethereum signing this offchain message", + }, + }); + + const response = await fetch("/api/register", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + signerAddress: address, + signature, + }), + }); + + if (response.ok) { + setHasSignedMessage(true); + } else { + console.error("Failed to register"); + } + } catch (error) { + console.error("Error signing message:", error); + } + }; + return ( <>
@@ -69,14 +104,27 @@ const Home: NextPage = () => {

3. Sign your first message

{hasSignedMessage && }
-

Try out a simple off-chain signature - no gas fees!

- + {!hasSignedMessage ? ( + <> +

Try out a simple off-chain signature - no gas fees!

+ + + ) : ( + <> +

+ You have signed the message successfully! Go to your profile to continue your journey: +

+ + + + + )} diff --git a/packages/nextjs/services/database/repositories/users.ts b/packages/nextjs/services/database/repositories/users.ts index 5971492..eba0e78 100644 --- a/packages/nextjs/services/database/repositories/users.ts +++ b/packages/nextjs/services/database/repositories/users.ts @@ -1,11 +1,12 @@ import { InferInsertModel } from "drizzle-orm"; +import { eq } from "drizzle-orm"; import { db } from "~~/services/database/config/postgresClient"; import { users } from "~~/services/database/config/schema"; export type UserInsert = InferInsertModel; -export async function getAllUsers() { - return await db.select().from(users); +export async function findUserByAddress(address: string) { + return await db.select().from(users).where(eq(users.id, address)); } export async function createUser(user: UserInsert) { diff --git a/packages/nextjs/utils/eip712.ts b/packages/nextjs/utils/eip712.ts new file mode 100644 index 0000000..b103390 --- /dev/null +++ b/packages/nextjs/utils/eip712.ts @@ -0,0 +1,11 @@ +export const EIP_712_DOMAIN = { + name: "Start Ethereum", + version: "1", +} as const; + +export const EIP_712_TYPES__REGISTER = { + Message: [ + { name: "action", type: "string" }, + { name: "description", type: "string" }, + ], +};