Skip to content

Commit

Permalink
Offchain signature => register user
Browse files Browse the repository at this point in the history
  • Loading branch information
carletex committed Dec 24, 2024
1 parent 82e8068 commit 597487b
Show file tree
Hide file tree
Showing 4 changed files with 124 additions and 11 deletions.
53 changes: 53 additions & 0 deletions packages/nextjs/app/api/register/route.ts
Original file line number Diff line number Diff line change
@@ -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 });
}
}
66 changes: 57 additions & 9 deletions packages/nextjs/app/page.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
<div className="flex items-center flex-col flex-grow pt-10">
Expand Down Expand Up @@ -69,14 +104,27 @@ const Home: NextPage = () => {
<h2 className="text-2xl font-bold">3. Sign your first message</h2>
{hasSignedMessage && <CheckCircleIcon className="text-green-500 h-5 w-5" />}
</div>
<p className="text-gray-600">Try out a simple off-chain signature - no gas fees!</p>
<button
className="btn btn-primary btn-sm w-fit"
disabled={!isConnected}
onClick={() => setHasSignedMessage(true)}
>
Sign Message
</button>
{!hasSignedMessage ? (
<>
<p className="text-gray-600">Try out a simple off-chain signature - no gas fees!</p>
<button
className="btn btn-primary btn-sm w-fit"
disabled={!isConnected || isSigningMessage}
onClick={handleSignMessage}
>
{isSigningMessage ? <span className="loading loading-spinner"></span> : "Sign Message"}
</button>
</>
) : (
<>
<p className="text-gray-600">
You have signed the message successfully! Go to your profile to continue your journey:
</p>
<Link href={`/profile/${address}`}>
<button className="btn btn-primary btn-sm w-fit">Go to Profile</button>
</Link>
</>
)}
</div>
</div>
</div>
Expand Down
5 changes: 3 additions & 2 deletions packages/nextjs/services/database/repositories/users.ts
Original file line number Diff line number Diff line change
@@ -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<typeof users>;

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) {
Expand Down
11 changes: 11 additions & 0 deletions packages/nextjs/utils/eip712.ts
Original file line number Diff line number Diff line change
@@ -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" },
],
};

0 comments on commit 597487b

Please sign in to comment.