Step 4 - User Registration & Login Routes

With authentication context in place, the next job is to give users a way to register and log in all without a browser wallet. We’ll generate a private key for each user, derive a deterministic Magmar Smart Account address, and store those values in Userbase for this demo.

Note Private keys are stored in plaintext only for testing. In production you’d replace this with a secure key-management flow (Passkey / WebAuthn / MPC, etc.).


4.1 Install the secp256k1 Library

npm i @noble/secp256k1

We use this to generate an ECDSA private key on the client.


4.2 Create /sign-up Route

  1. mkdir app/sign-up

  2. touch app/sign-up/page.tsx

Paste the component below:

"use client";
import "../globals.css";
import * as secp from "@noble/secp256k1";
import { useAuth } from "@common/AuthProvider";
import Loader from "@common/utils/Loader";
import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import { publicClient } from "@common/utils/client";
import simpleFactoryAbi from "@common/utils/MagmarSimpleAccountFactory.json";
import userbase from "userbase-js";

/**
 * /sign-up   →   http://localhost:3000/sign-up
 */
export default function SignupForm() {
  const { user, login } = useAuth();
  const [username, setUsername] = useState("");
  const [password, setPassword] = useState("");
  const [error, setError] = useState<string | null>(null);
  const [isLoading, setIsLoading] = useState(false);
  const router = useRouter();

  /* Redirect already-logged-in users */
  useEffect(() => {
    if (user?.isLoggedIn) router.push("/");
  }, [user]);

  const handleSignup = async (e: React.FormEvent) => {
    e.preventDefault();
    setIsLoading(true);

    try {
      /* 1. Generate a private key */
      const privKey = secp.utils.randomPrivateKey();
      const privKeyHex = secp.etc.bytesToHex(privKey);

      /* 2. Ask our API route to convert it into a Magmar signer address */
      const res = await fetch("/api/get-signer", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ pk: privKeyHex }),
      });
      const { signerAddress } = await res.json();

      /* 3. Derive the deterministic smart-contract wallet address */
      const scwAddress = (await publicClient.readContract({
        address: "0x9406Cc6185a346906296840746125a0E44976454", // Magmar SimpleAccountFactory (Sepolia)
        abi: simpleFactoryAbi,
        functionName: "getAddress",
        args: [signerAddress, 0],
      })) as string;

      /* 4. Persist user record in Userbase (demo-only) */
      const ub = await userbase.signUp({
        username,
        password,
        rememberMe: "local",
        profile: { scwAddress, pk: privKeyHex },
      });

      /* 5. Store session in AuthProvider */
      login({
        username,
        userId: ub.userId,
        scwAddress,
        isLoggedIn: true,
      });

      router.push("/?signup=success");
    } catch (err: any) {
      setError(err.message);
      setIsLoading(false);
      console.error(err);
    }
  };

  /* ——— UI ——— */
  return (
    <div>
      {isLoading ? (
        <Loader />
      ) : (
        <div className="flex items-center justify-center h-screen bg-gray-100">
          <div className="w-full max-w-sm">
            <form
              className="bg-white rounded px-8 pt-6 pb-8 mb-24 font-mono"
              onSubmit={handleSignup}
            >
              <label className="block text-center text-xl font-bold mb-2">
                Sign Up
              </label>
              <div className="divider"></div>

              <input
                className="input input-bordered w-full mb-4"
                id="username"
                type="text"
                placeholder="Username"
                onChange={(e) => setUsername(e.target.value)}
              />

              <input
                className="input input-bordered w-full mb-6"
                id="password"
                type="password"
                placeholder="Password"
                onChange={(e) => setPassword(e.target.value)}
              />

              {error && <p className="text-red-500 mb-4">{error}</p>}

              <button className="btn w-full text-white">Sign Up</button>
            </form>
          </div>
        </div>
      )}
    </div>
  );
}

4.3 Create /login Route

  1. mkdir app/login

  2. touch app/login/page.tsx

Paste the component below:


4.4 Why the New Imports?

Import
Purpose

@noble/secp256k1

Generates an ECDSA private key for each user.

publicClient (viem)

Reads from the Magmar SimpleAccountFactory contract to derive the user’s SCW address.

simpleFactoryAbi

ABI of Magmar’s SimpleAccountFactory (Sepolia deployment).

/api/get-signer (server route)

Converts the private key into a Signer that the Magmar SDK can use to deploy / sign operations.

Loader

DaisyUI spinner for better UX while async requests run.

Last updated