Commands

Wallet Auth (Sign in with Ethereum)

Wallet Auth is our native support for Sign in With Ethereum.

World App currently uses Safe addresses in order to do the SIWE flow. This means that it will not work with common libraries like SIWE which expect EOAs.

Creating the nonce

Since the user can modify the client, it's important to create the nonce in the backend. The nonce must be at least 8 alphanumeric characters in length.

app/api/nonce.ts

import { cookies } from "next/headers";
import { NextRequest, NextResponse } from "next/server";

export function GET(req: NextRequest) {
  // Expects only alphanumeric characters
  const nonce = crypto.randomUUID().replace(/-/g, "");

  // The nonce should be stored somewhere that is not tamperable by the client
  // Optionally you can HMAC the nonce with a secret key stored in your environment
  cookies().set("siwe", nonce, { secure: true });
  return NextResponse.json({ nonce });
}

Sending the command

Below is the expected input for walletAuth.

interface WalletAuthInput {
  nonce: string
  expirationTime?: Date
  statement?: string
  requestId?: string
  notBefore?: Date
} 

Using the walletAuth command.

app/page.tsx

import { MiniKit, WalletAuthInput } from '@worldcoin/minikit-js'
  // ...
  const signInWithWallet = async () => {
    const res = await fetch(`/api/nonce`);
    const { nonce } = await res.json();

    const generateMessageResult = MiniKit.commands.walletAuth({
      nonce: nonce,
      requestId: "0", // Optional
      expirationTime: new Date(new Date().getTime() + 7 * 24 * 60 * 60 * 1000),
      notBefore: new Date(new Date().getTime() - 24 * 60 * 60 * 1000),
      statement:
        "This is my statement and here is a link https://worldcoin.com/apps",
    });
  };

Receiving the response

The returned message will include a signature compliant with ERC-191. You're welcome to use any third party libraries to verify the payloads for SIWE, but since World App uses Safe addresses that do not implement isValidSignature we created a helper function for you. Under the hood, we use the wallet's EOA private key to sign the message but the address is the Safe address.

type MiniAppWalletAuthSuccessPayload = {
  status: "success";
  message: string;
  signature: string;
  address: string;
  version: number;
};

app/page.tsx

import { ResponseEvent } from '@worldcoin/minikit-js'
  // ...
  useEffect(() => {
    if (!MiniKit.isInstalled()) {
      return;
    }

    MiniKit.subscribe(ResponseEvent.MiniAppWalletAuth, async (payload) => {
      if (payload.status === "error") {
        return
      } else {
        const response = await fetch("/api/complete-siwe", {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
          },
          body: JSON.stringify({
            payload: payload,
            nonce,
          }),
        });
      }
    });

    return () => {
      MiniKit.unsubscribe(ResponseEvent.MiniAppWalletAuth);
    };
  }, []);

You can now additionally access the user's wallet address from the minikit object.

const walletAddress = MiniKit.walletAddress;
// or
const walletAddress = window.MiniKit?.walletAddress;

Verifying the Login

Finally, complete the sign in by verifying the response from World App in your backend. Here we check the nonce matches the one we created earlier, and then verify the signature.

app/api/complete-siwe.ts

import { MiniAppWalletAuthSuccessPayload, verifySiweMessage } from "@worldcoin/minikit-js";
import { cookies } from "next/headers";
import { NextRequest, NextResponse } from "next/server";

interface IRequestPayload {
  payload: MiniAppWalletAuthSuccessPayload;
  nonce: string;
}

export const POST = async (req: NextRequest) => {
  const { payload, nonce } = (await req.json()) as IRequestPayload;
  if (nonce != cookies().get("siwe")?.value) {
    return NextResponse.json({
      status: "error",
      isValid: false,
      message: "Invalid nonce",
    });
  }
  try {
    const validMessage = await verifySiweMessage(payload, nonce);
    return NextResponse.json({
      status: "success",
      isValid: validMessage.isValid,
    });
  } catch (error: any) {
    // Handle errors in validation or processing
    return NextResponse.json({
      status: "error",
      isValid: false,
      message: error.message,
    });
  }
};