Skip to content

ZeroDev Tutorial -- Passkeys

In this tutorial, we will be building a Next.js app where your users can create smart accounts and send UserOps with passkeys.

Clone the template

We have prepared a Next.js template for you:

git clone git@github.com:zerodevapp/passkey-tutorial.git

If you ever want to check out the completed code, you can checkout the completed branch. You can also see a deployed demo here.

Run the app in development mode:

npm i
npm run dev

Open app/page.tsx. We will be working on this file for the rest of the tutorial.

Create a ZeroDev project

For this tutorial, create a ZeroDev project for Sepolia.

On app/page.tsx, fill out these variables with the URLs found on your dashboard.

const BUNDLER_URL = ""
const PAYMASTER_URL = ""
const PASSKEY_SERVER_URL = ""

Create a smart account using passkeys

Let's hook up the Register and Login buttons so they actually do something.

In handleRegister, add the following code:

// Function to be called when "Register" is clicked
const handleRegister = async () => {
  setIsRegistering(true)
 
  const webAuthnKey = await toWebAuthnKey({ 
            passkeyName: username, 
            passkeyServerUrl: PASSKEY_SERVER_URL, 
            mode: WebAuthnMode.Register, 
            passkeyServerHeaders: {} 
  }) 
 
  const passkeyValidator = await toPasskeyValidator(publicClient, {  
    webAuthnKey,   
    entryPoint,   
    kernelVersion: KERNEL_V3_1,   
    validatorContractVersion: PasskeyValidatorContractVersion.V0_0_2
  })   
 
  await createAccountAndClient(passkeyValidator)  
 
  setIsRegistering(false)
  window.alert('Register done.  Try sending UserOps.')
}

And in handleLogin, add the following code:

const handleLogin = async () => {
  setIsLoggingIn(true)
 
  const webAuthnKey = await toWebAuthnKey({ 
            passkeyName: username, 
            passkeyServerUrl: PASSKEY_SERVER_URL, 
            mode: WebAuthnMode.Login, 
            passkeyServerHeaders: {} 
  }) 
 
  const passkeyValidator = await toPasskeyValidator(publicClient, { 
    webAuthnKey,  
    entryPoint, 
    kernelVersion: KERNEL_V3_1,   
    validatorContractVersion: PasskeyValidatorContractVersion.V0_0_2
  })  
 
  await createAccountAndClient(passkeyValidator)  
 
  setIsLoggingIn(false)
  window.alert('Login done.  Try sending UserOps.')
}

In this tutorial, we are using a public passkey server URL. In practice, you'd create your own passkey server URL from the dashboard.

Now modify createAccountAndClient to actually create the account using the passkeyValidator:

const createAccountAndClient = async (passkeyValidator: any) => {
  kernelAccount = await createKernelAccount(publicClient, { 
    plugins: { 
      sudo: passkeyValidator, 
    }, 
    entryPoint, 
    kernelVersion: KERNEL_V3_1
  }) 
 
  kernelClient = createKernelAccountClient({ 
    account: kernelAccount, 
    chain: CHAIN, 
    bundlerTransport: http(BUNDLER_URL), 
    client: publicClient, 
    paymaster: { 
      getPaymasterData: (userOperation) => { 
        const zerodevPaymaster = createZeroDevPaymasterClient({ 
          chain: CHAIN, 
          transport: http(PAYMASTER_URL), 
        }) 
        return zerodevPaymaster.sponsorUserOperation({ 
          userOperation, 
        }) 
      } 
    }, 
    userOperation: { 
      estimateFeesPerGas: async ({bundlerClient}) => { 
        return getUserOperationGasPrice(bundlerClient) 
      } 
    } 
  }) 
 
  setIsKernelClientReady(true) 
  setAccountAddress(kernelAccount.address) 
}

At this point, you should be able to create passkey accounts with either Register or Login.

Sending UserOps

Sending UserOps from a passkey account is the same as sending them from any account. Modify handleSendUserOp as such:

const handleSendUserOp = async () => {
  setIsSendingUserOp(true)
  setUserOpStatus('Sending UserOp...')
 
  const userOpHash = await kernelClient.sendUserOperation({ 
      callData: await kernelAccount.encodeCalls([{ 
        to: contractAddress, 
        value: BigInt(0), 
        data: encodeFunctionData({ 
          abi: contractABI, 
          functionName: "mint", 
          args: [kernelAccount.address], 
        }), 
      }]), 
  }) 
 
  setUserOpHash(userOpHash)
 
  await kernelClient.waitForUserOperationReceipt({ 
    hash: userOpHash, 
  }) 
 
  // Update the message based on the count of UserOps
  const userOpMessage = `UserOp completed. <a href="https://jiffyscan.xyz/userOpHash/${userOpHash}?network=sepolia" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-700">Click here to view.</a>`
 
  setUserOpStatus(userOpMessage)
  setIsSendingUserOp(false)
}

Now try sending some UserOps!

Also, the UserOps are sponsored thanks to paymasters -- that's why you are able to send UserOps from an account with no ETH.

Next Steps

In this tutorial, you were able to create smart accounts and send UserOps with passkeys.

For next steps: