DEV Community

Cover image for How to Build a Secure ENS Domain Registration App with React and Wagmi
Radzion Chachura
Radzion Chachura

Posted on • Originally published at radzion.com

How to Build a Secure ENS Domain Registration App with React and Wagmi

🐙 GitHub | 🎮 Demo

Introduction

Let's build a straightforward application that allows you to purchase ENS names like radzion.eth by interacting directly with ENS smart contracts using wagmi and viem. You can explore the complete code on GitHub and try out the live demo here. To streamline our development process, we'll use RadzionKit as our boilerplate, giving us access to all the necessary utilities and components.

Setting Up the Application

Our application needs to perform several blockchain operations, from checking name availability to executing purchase transactions. To enable these features, we'll set up two essential providers at the root of our app: the WagmiProvider for handling blockchain interactions, and the RainbowKitProvider for managing wallet connections.

import { Page } from "@lib/next-ui/Page"
import { GlobalStyle } from "@lib/ui/css/GlobalStyle"
import { DarkLightThemeProvider } from "@lib/ui/theme/DarkLightThemeProvider"
import { RainbowKitProvider } from "@rainbow-me/rainbowkit"
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
import { Inter } from "next/font/google"
import { ReactNode, useState } from "react"
import { WagmiProvider } from "wagmi"

import { wagmiConfig } from "../chain/wagmiConfig"

import type { AppProps } from "next/app"

const inter = Inter({
  subsets: ["latin"],
  weight: ["400", "500", "600", "800"],
})

interface MyAppProps extends AppProps {
  Component: Page
}

function MyApp({ Component, pageProps }: MyAppProps) {
  const [queryClient] = useState(() => new QueryClient())

  const getLayout = Component.getLayout || ((page: ReactNode) => page)
  const component = getLayout(<Component {...pageProps} />)

  return (
    <QueryClientProvider client={queryClient}>
      <DarkLightThemeProvider value="dark">
        <GlobalStyle fontFamily={inter.style.fontFamily} />
        <WagmiProvider config={wagmiConfig}>
          <RainbowKitProvider>{component}</RainbowKitProvider>
        </WagmiProvider>
      </DarkLightThemeProvider>
    </QueryClientProvider>
  )
}

export default MyApp
Enter fullscreen mode Exit fullscreen mode

Configuring Blockchain Interactions

The wagmiConfig object configures the core settings for our blockchain interactions. It defines our application's identity through the appName, sets up authentication with Reown (formerly WalletConnect) using a project ID, specifies which blockchain networks we support, and establishes HTTP transport layers for communicating with each chain.

import { shouldBePresent } from "@lib/utils/assert/shouldBePresent"
import { recordFromKeys } from "@lib/utils/record/recordFromKeys"
import { getDefaultConfig } from "@rainbow-me/rainbowkit"
import "@rainbow-me/rainbowkit/styles.css"
import { http } from "viem"

import { productName } from "../product/config"

import { chains } from "."

export const wagmiConfig = getDefaultConfig({
  appName: productName,
  projectId: shouldBePresent(process.env.NEXT_PUBLIC_REOWN_PROJECT_ID),
  chains,
  transports: recordFromKeys(
    chains.map((chain) => chain.id),
    () => http(),
  ),
})
Enter fullscreen mode Exit fullscreen mode

Network Configuration and Type Safety

Our application will operate on two Ethereum networks: the Mainnet for production use and the Sepolia Testnet for development. To ensure type safety and maintain consistency across our codebase, we'll create a strongly-typed ChainId type derived from our supported chains. This type system will help us catch potential errors at compile time, ensuring we've properly configured contract addresses for all supported networks.

import { findBy } from "@lib/utils/array/findBy"
import { shouldBePresent } from "@lib/utils/assert/shouldBePresent"
import { sepolia, mainnet } from "viem/chains"

export const chains = [sepolia, mainnet] as const

export const chainIds = chains.map((c) => c.id)

export type ChainId = (typeof chainIds)[number]

export const getChain = (chainId: number) =>
  shouldBePresent(findBy(chains, "id", chainId))
Enter fullscreen mode Exit fullscreen mode

Implementing the Registration Flow

Let's implement the ENS registration process as a two-step flow for a smooth user experience. In the first step, users will interact with a form to select their desired ENS name and registration duration. The second step handles the blockchain transaction, providing real-time progress updates and concluding with either a success or failure screen, ensuring users stay informed throughout the process.

import { Flow } from "@lib/ui/base/Flow"
import { useState } from "react"

import { NameRegistrationParams } from "../NameRegistrationParams"

import { RegistrationFlowExecutionStep } from "./RegistrationFlowExecutionStep"
import { RegistrationFlowNameStep } from "./RegistrationFlowNameStep"

type RegistrationFlowStep =
  | {
      id: "name"
      name: string
    }
  | {
      id: "execution"
      params: NameRegistrationParams
    }

export const RegistrationFlow = () => {
  const [step, setStep] = useState<RegistrationFlowStep>({
    id: "name",
    name: "",
  })

  return (
    <Flow
      step={step}
      steps={{
        name: () => (
          <RegistrationFlowNameStep
            onFinish={(params) => setStep({ id: "execution", params })}
          />
        ),
        execution: ({ params }) => (
          <RegistrationFlowExecutionStep
            onBack={() => setStep({ id: "name", name: params.name })}
            onFinish={() => setStep({ id: "name", name: "" })}
            params={params}
          />
        ),
      }}
    />
  )
}
Enter fullscreen mode Exit fullscreen mode

Managing Multi-Step Flow

We're leveraging the abstract Flow component from RadzionKit to manage our multi-step registration process in a type-safe and decoupled manner. This component eliminates the need for a complex state management provider by handling step transitions locally. Each step is implemented as an independent component with its own state and logic, communicating with other steps only through well-defined callbacks. This architecture ensures clean separation of concerns and makes our code more maintainable – the name selection form doesn't need to know about transaction execution details, and vice versa.

import { ReactNode } from "react"

export interface FlowStep<K extends string> {
  id: K
}

type StepHandlers<K extends string, T extends FlowStep<K>> = {
  [key in T["id"]]: (step: Extract<T, { id: key }>) => ReactNode
}

type FlowProps<K extends string, T extends FlowStep<K>> = {
  step: T
  steps: StepHandlers<K, T>
}

export function Flow<K extends string, T extends FlowStep<K>>({
  step,
  steps,
}: FlowProps<K, T>) {
  const id = step.id
  const render = steps[id]

  return <>{render(step as Extract<T, { id: K }>)}</>
}
Enter fullscreen mode Exit fullscreen mode

Building the Name Selection Interface

The RegistrationFlowNameStep component implements a progressive disclosure pattern to streamline the user experience. Initially, it presents a clean interface with just a text input for the ENS name. As users type, the component automatically checks name availability in real-time. Once a valid and available name is found, the interface expands to reveal additional options: a duration selector and a registration button. This step-by-step revelation helps users focus on one decision at a time while preventing them from proceeding with unavailable names.

ENS Registration Form

import { Button } from "@lib/ui/buttons/Button"
import { VStack } from "@lib/ui/css/stack"
import { getFormProps } from "@lib/ui/form/utils/getFormProps"
import { InputDebounce } from "@lib/ui/inputs/InputDebounce"
import { TextInput } from "@lib/ui/inputs/TextInput"
import { Center } from "@lib/ui/layout/Center"
import { OnFinishProp } from "@lib/ui/props"
import { MatchQuery } from "@lib/ui/query/components/MatchQuery"
import { Text } from "@lib/ui/text"
import { addYears, differenceInSeconds } from "date-fns"
import { useState } from "react"

import { WalletDependantForm } from "../../chain/wallet/components/WalletDependantForm"
import { tld } from "../config"
import { NameRegistrationParams } from "../NameRegistrationParams"
import { useIsNameAvailableQuery } from "../queries/useIsNameAvailableQuery"
import { useRegistrationDuration } from "../state/registrationDuration"

import { ManageRegistrationDuration } from "./ManageRegistrationDuration"
import { RegistrationStepContainer } from "./RegistrationStepContainer"
import { RegistrationStepTitle } from "./RegistrationStepTitle"

export const RegistrationFlowNameStep = ({
  onFinish,
}: OnFinishProp<NameRegistrationParams>) => {
  const [name, setName] = useState("")

  const isNameAvailableQuery = useIsNameAvailableQuery(name)

  const [duration] = useRegistrationDuration()

  return (
    <Center>
      <WalletDependantForm
        submitText="Register"
        onSubmit={({ walletClient }) => {
          const now = new Date()
          onFinish({
            name,
            walletClient,
            duration: differenceInSeconds(addYears(now, duration), now),
          })
        }}
        render={({ submitText, onSubmit }) => (
          <RegistrationStepContainer
            as="form"
            {...getFormProps({
              isDisabled: !isNameAvailableQuery.data,
              onSubmit,
            })}
          >
            <RegistrationStepTitle>Your web3 username</RegistrationStepTitle>
            <VStack gap={8}>
              <InputDebounce<string>
                value={name}
                onChange={setName}
                render={({ value, onChange }) => (
                  <TextInput
                    value={value}
                    onValueChange={(newValue) =>
                      onChange(newValue.replace(/[.\s]/g, ""))
                    }
                    autoFocus
                    placeholder={`Search for a .${tld} name`}
                  />
                )}
              />
              {name && (
                <MatchQuery
                  value={isNameAvailableQuery}
                  pending={() => "Checking availability..."}
                  success={(isAvailable) => {
                    const fullName = `${name}.${tld}`

                    return (
                      <Text color={isAvailable ? "success" : "alert"}>
                        {isAvailable
                          ? `${fullName} is available!`
                          : `${fullName} is already taken.`}
                      </Text>
                    )
                  }}
                  error={() => "Error checking availability."}
                />
              )}
            </VStack>
            {!!isNameAvailableQuery.data && (
              <>
                <ManageRegistrationDuration />
                <Button type="submit">{submitText}</Button>
              </>
            )}
          </RegistrationStepContainer>
        )}
      />
    </Center>
  )
}
Enter fullscreen mode Exit fullscreen mode

Checking Name Availability

To verify ENS name availability, we interact with the ETHRegistrarController smart contract using wagmi's useReadContract hook. This hook provides a seamless integration with the blockchain, automatically managing the query lifecycle through react-query. By calling the contract's available function with the desired name, we receive real-time availability status while benefiting from react-query's built-in caching and revalidation features.

import { type Abi } from "viem"
import { useReadContract } from "wagmi"

import { ChainId } from "../../chain"
import { useChainId } from "../../chain/hooks/useChainId"
import {
  ethRegistrarControllerAbi,
  ethRegistrarControllerAddresses,
} from "../contracts/ethRegistrarConroller"

export const useIsNameAvailableQuery = (name: string) => {
  const chainId = useChainId()

  return useReadContract({
    address: ethRegistrarControllerAddresses[chainId as ChainId],
    abi: ethRegistrarControllerAbi as Abi,
    functionName: "available",
    args: [name],
    query: {
      enabled: !!name,
    },
  })
}
Enter fullscreen mode Exit fullscreen mode

Handling Network Selection

To enhance the user experience, we've implemented the useChainId hook to ensure seamless name availability checks regardless of wallet connection status. This hook intelligently determines which network to query by first checking for a connected wallet's network (if it's supported) and gracefully falling back to our default supported chain when no wallet is connected. This approach allows users to explore name availability before connecting their wallet, reducing friction in the discovery phase.

import { isOneOf } from "@lib/utils/array/isOneOf"
import { useAccount } from "wagmi"

import { ChainId, chainIds } from ".."

export const useChainId = (): ChainId => {
  const { chainId } = useAccount()

  return isOneOf(chainId, chainIds) ?? chainIds[0]
}
Enter fullscreen mode Exit fullscreen mode

Smart Contract Integration

The ETHRegistrarController contract uses the same ABI across networks but has different deployment addresses. We map these addresses to their respective chain IDs in a type-safe configuration.

import { mainnet, sepolia } from "viem/chains"

import { ChainId } from "../../chain"

export const ethRegistrarControllerAddresses: Record<ChainId, `0x${string}`> = {
  [mainnet.id]: "0x253553366Da8546fC250F225fe3d25d0C782303b",
  [sepolia.id]: "0xFED6a969AaA60E4961FCD3EBF1A2e8913ac65B72",
}

export const ethRegistrarControllerAbi = [
  // a long abi ...
]
Enter fullscreen mode Exit fullscreen mode

Managing Wallet States

The WalletDependantForm component uses the render prop pattern to consistently handle wallet states while maintaining the same UI structure. It always renders your form but adapts its behavior based on the wallet state: without a wallet, the submit button becomes a "Connect Wallet" trigger; on an unsupported network, it becomes a "Switch Network" trigger; and only with a proper connection does it execute the actual form submission. This approach ensures a seamless user experience while preserving your form's visual consistency across all wallet states.

Connect Wallet

import { IsDisabledProp } from "@lib/ui/props"
import { MatchQuery } from "@lib/ui/query/components/MatchQuery"
import { ConnectButton } from "@rainbow-me/rainbowkit"
import {
  useAccount,
  useSwitchChain,
  useWalletClient,
  UseWalletClientReturnType,
} from "wagmi"

import { chains } from "../.."

type SubmitParams = {
  walletClient: NonNullable<UseWalletClientReturnType["data"]>
}

type RenderParams = IsDisabledProp & {
  submitText: string
  onSubmit: () => void
}

type WalletDependantFormProps = IsDisabledProp & {
  submitText: string
  onSubmit: (params: SubmitParams) => void
  render: (params: RenderParams) => React.ReactNode
}

export const WalletDependantForm = ({
  render,
  submitText,
  onSubmit,
  isDisabled,
}: WalletDependantFormProps) => {
  const { isConnected, chainId } = useAccount()

  const { switchChain } = useSwitchChain()

  const walletClientQuery = useWalletClient()

  if (!isConnected) {
    return (
      <ConnectButton.Custom>
        {({ openConnectModal }) =>
          render({
            submitText: "Connect Wallet",
            onSubmit: openConnectModal,
          })
        }
      </ConnectButton.Custom>
    )
  }

  const isSupportedChain = chains.some((chain) => chain.id === chainId)

  if (!isSupportedChain) {
    return render({
      isDisabled: true,
      submitText: "Switch Network",
      onSubmit: () => switchChain({ chainId: chains[0].id }),
    })
  }

  return (
    <MatchQuery
      value={walletClientQuery}
      success={(walletClient) =>
        render({
          submitText,
          isDisabled,
          onSubmit: () => onSubmit({ walletClient }),
        })
      }
    />
  )
}
Enter fullscreen mode Exit fullscreen mode

Transaction Execution and Progress Tracking

Once the form is submitted, our application transitions to the RegistrationFlowExecutionStep component, which handles the crucial transaction execution phase. This component automatically initiates the transaction when mounted and leverages our familiar MatchQuery pattern to render appropriate UI states. Whether the transaction is pending, successful, or encounters an error, users receive clear visual feedback throughout the entire registration process.

import { Button } from "@lib/ui/buttons/Button"
import { ProgressList } from "@lib/ui/progress/list/ProgressList"
import { OnBackProp, OnFinishProp } from "@lib/ui/props"
import { MatchQuery } from "@lib/ui/query/components/MatchQuery"
import { Text } from "@lib/ui/text"
import { useEffect } from "react"

import {
  useRegisterNameMutation,
  nameRegistrationSteps,
  nameRegistrationStepText,
} from "../mutations/useRegisterNameMutation"
import { NameRegistrationParams } from "../NameRegistrationParams"

import { RegistrationFlowFailureState } from "./RegistrationFlowFailureState"
import { RegistrationFlowSuccessState } from "./RegistrationFlowSuccessState"
import { RegistrationStepContainer } from "./RegistrationStepContainer"
import { RegistrationStepTitle } from "./RegistrationStepTitle"

type RegistrationFlowExecutionStepProps = OnBackProp &
  OnFinishProp & {
    params: NameRegistrationParams
  }

export const RegistrationFlowExecutionStep = ({
  onBack,
  onFinish,
  params,
}: RegistrationFlowExecutionStepProps) => {
  const { mutate: register, step, ...mutationState } = useRegisterNameMutation()

  useEffect(() => {
    register(params)
  }, [params, register])

  return (
    <MatchQuery
      value={mutationState}
      success={() => (
        <RegistrationFlowSuccessState value={params.name} onFinish={onFinish} />
      )}
      pending={() => (
        <RegistrationStepContainer alignItems="center">
          <RegistrationStepTitle>
            Registering {params.name}.eth
          </RegistrationStepTitle>
          <Text centerHorizontally>
            Please wait while we process your registration. This may take a few
            minutes.
          </Text>
          {step && (
            <ProgressList
              items={nameRegistrationSteps}
              value={step}
              renderItem={(item) => nameRegistrationStepText[item]}
            />
          )}
          <Button kind="secondary" onClick={onBack}>
            Cancel
          </Button>
        </RegistrationStepContainer>
      )}
      error={() => <RegistrationFlowFailureState onFinish={onBack} />}
    />
  )
}
Enter fullscreen mode Exit fullscreen mode

Visual Progress Indication

To enhance the user experience during the registration process, we implement a dynamic progress indicator using the step variable returned by our mutation. This variable drives the ProgressList component, which renders a visual timeline of the registration steps. As the transaction progresses through different stages, the list automatically updates to highlight the current step and mark completed ones, providing users with clear visibility into their registration status.

Progress List

import { VStack } from "@lib/ui/css/stack"

import { ItemsProp, RenderItemProp, ValueProp } from "../../props"

import { ProgressListItem, ProgressListItemKind } from "./ProgressListItem"

type ProgressListProps<T extends string = string> = ValueProp<T> &
  ItemsProp<T> &
  RenderItemProp<T>

export const ProgressList = <T extends string = string>({
  items,
  value,
  renderItem,
}: ProgressListProps<T>) => {
  const currentStepIndex = items.findIndex((item) => item === value)

  return (
    <VStack gap={8}>
      {items.map((item, index) => {
        let kind: ProgressListItemKind = "pending"
        if (index < currentStepIndex) {
          kind = "completed"
        } else if (index === currentStepIndex) {
          kind = "active"
        }
        return (
          <ProgressListItem key={item} kind={kind}>
            {renderItem(item)}
          </ProgressListItem>
        )
      })}
    </VStack>
  )
}
Enter fullscreen mode Exit fullscreen mode

Each step in our progress list is rendered by the ProgressListItem component, which provides visual cues about the step's status. The component uses styled-components to create a consistent visual hierarchy: completed steps show a checkmark in green, the active step displays an animated spinner, and pending steps remain empty.

import styled from "styled-components"

import { Match } from "../../base/Match"
import { centerContent } from "../../css/centerContent"
import { sameDimensions } from "../../css/sameDimensions"
import { HStack } from "../../css/stack"
import { CheckIcon } from "../../icons/CheckIcon"
import { Spinner } from "../../loaders/Spinner"
import { ChildrenProp, KindProp } from "../../props"
import { Text, TextColor } from "../../text"
import { getColor } from "../../theme/getters"

export type ProgressListItemKind = "completed" | "active" | "pending"

const IndicatorContainer = styled.div`
  ${sameDimensions(24)}
  ${centerContent}
`

const CompletedIndicator = styled(CheckIcon)`
  color: ${getColor("success")};
`

const ActiveIndicator = styled(Spinner)`
  color: ${getColor("contrast")};
`

const kindToColor: Record<ProgressListItemKind, TextColor> = {
  completed: "regular",
  active: "contrast",
  pending: "shy",
}

export const ProgressListItem = ({
  kind,
  children,
}: KindProp<ProgressListItemKind> & ChildrenProp) => {
  return (
    <HStack gap={12} alignItems="center">
      <IndicatorContainer>
        <Match
          value={kind}
          completed={() => <CompletedIndicator />}
          active={() => <ActiveIndicator />}
          pending={() => <div />}
        />
      </IndicatorContainer>
      <Text color={kindToColor[kind]}>{children}</Text>
    </HStack>
  )
}
Enter fullscreen mode Exit fullscreen mode

The success and error states share a similar structure but implement different navigation behaviors. The success state clears the form and returns to the initial step, while the error state preserves the entered name for retry. Both states utilize the RegistrationStepContainer for consistent layout and styling.

Registration Flow Success State

import { Button } from "@lib/ui/buttons/Button"
import { getFormProps } from "@lib/ui/form/utils/getFormProps"
import { CheckCircleIcon } from "@lib/ui/icons/CheckCircleIcon"
import { OnFinishProp, ValueProp } from "@lib/ui/props"
import { Text } from "@lib/ui/text"

import { tld } from "../config"

import { RegistrationStepContainer } from "./RegistrationStepContainer"
import { RegistrationStepTitle } from "./RegistrationStepTitle"

export const RegistrationFlowSuccessState = ({
  value,
  onFinish,
}: ValueProp<string> & OnFinishProp) => {
  return (
    <RegistrationStepContainer
      as="form"
      {...getFormProps({ onSubmit: onFinish })}
      alignItems="center"
    >
      <Text color="success" size={40}>
        <CheckCircleIcon />
      </Text>

      <RegistrationStepTitle>Congratulations!</RegistrationStepTitle>
      <Text centerHorizontally color="contrast">
        You have successfully registered{" "}
        <strong>
          {value}.{tld}
        </strong>
      </Text>

      <Button type="submit">Back to Home</Button>
    </RegistrationStepContainer>
  )
}
Enter fullscreen mode Exit fullscreen mode

Implementing the Registration Process

At the core of our ENS registration system lies an elegant two-phase commit-reveal pattern – a clever blockchain mechanism that ensures fair name registration. When you decide to register a name, the process begins with a commitment phase: your desired name, chosen duration, and other registration parameters are hashed together and submitted to the blockchain, effectively reserving your intent without revealing the actual name. After a mandatory one-minute waiting period, the second phase begins where you reveal your true registration request along with the payment. This clever mechanism prevents front-running attacks, where others might try to snipe your desired name by seeing and copying your transaction. Our useRegisterNameMutation hook orchestrates this dance, managing everything from commitment creation to price calculations (with a 10% buffer added to account for price fluctuations) and final registration. To keep you in the loop, it maintains a clear step-by-step progress indicator throughout the entire process, from preparation to final confirmation.

import { getRandomHex } from "@lib/chain/crypto/getRandomHex"
import { placeholderEvmAddress } from "@lib/chain/evm/utils/address"
import { shouldBePresent } from "@lib/utils/assert/shouldBePresent"
import { sleep } from "@lib/utils/sleep"
import { convertDuration } from "@lib/utils/time/convertDuration"
import { useMutation } from "@tanstack/react-query"
import { useState } from "react"
import { type Abi } from "viem"
import { useAccount, usePublicClient } from "wagmi"

import { getChain } from "../../chain"
import { useChainId } from "../../chain/hooks/useChainId"
import {
  ethRegistrarControllerAbi,
  ethRegistrarControllerAddresses,
} from "../contracts/ethRegistrarConroller"
import { NameRegistrationParams } from "../NameRegistrationParams"

const data: `0x${string}`[] = []
const reverseRecord = false
const ownerControlledFuses = 0

export const nameRegistrationSteps = [
  "preparing",
  "committing",
  "waiting",
  "calculating",
  "registering",
  "confirming",
] as const
export type NameRegistrationStep = (typeof nameRegistrationSteps)[number]

export const nameRegistrationStepText: Record<NameRegistrationStep, string> = {
  preparing: "Preparing commitment",
  committing: "Committing registration",
  waiting: "Waiting for confirmation",
  calculating: "Calculating price",
  registering: "Registering name",
  confirming: "Finalizing registration",
}

export const useRegisterNameMutation = () => {
  const chainId = useChainId()
  const { address } = useAccount()
  const publicClient = shouldBePresent(usePublicClient())

  const [step, setStep] = useState<NameRegistrationStep | null>(null)

  const mutation = useMutation({
    mutationFn: async ({
      name,
      walletClient,
      duration,
    }: NameRegistrationParams) => {
      const contractAddress = ethRegistrarControllerAddresses[chainId]

      const secret = getRandomHex(32)

      const commitmentHash = (await publicClient.readContract({
        address: contractAddress,
        abi: ethRegistrarControllerAbi as Abi,
        functionName: "makeCommitment",
        args: [
          name,
          address,
          duration,
          secret,
          placeholderEvmAddress,
          data,
          reverseRecord,
          ownerControlledFuses,
        ],
      })) as `0x${string}`

      setStep("committing")
      const hash = await walletClient.writeContract({
        address: contractAddress,
        abi: ethRegistrarControllerAbi as Abi,
        functionName: "commit",
        args: [commitmentHash],
        chain: getChain(chainId),
        account: address,
      })

      setStep("waiting")
      await publicClient.waitForTransactionReceipt({ hash })

      await sleep(convertDuration(1, "min", "ms"))

      setStep("calculating")
      const rentPriceResult = (await publicClient.readContract({
        address: contractAddress,
        abi: ethRegistrarControllerAbi as Abi,
        functionName: "rentPrice",
        args: [name, duration],
      })) as { base: bigint; premium: bigint }

      const totalPrice =
        ((rentPriceResult.base + rentPriceResult.premium) * BigInt(110)) /
        BigInt(100)

      setStep("registering")
      const registerHash = await walletClient.writeContract({
        address: contractAddress,
        abi: ethRegistrarControllerAbi as Abi,
        functionName: "register",
        args: [
          name,
          address,
          duration,
          secret,
          placeholderEvmAddress,
          data,
          reverseRecord,
          ownerControlledFuses,
        ],
        value: totalPrice,
        chain: getChain(chainId),
        account: address,
      })

      setStep("confirming")
      await publicClient.waitForTransactionReceipt({ hash: registerHash })

      return name
    },
    onMutate: () => {
      setStep("preparing")
    },
    onSettled: () => {
      setStep(null)
    },
  })

  return {
    ...mutation,
    step,
  }
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

In this tutorial, we've built a secure ENS registration application that combines modern web3 tools with an intuitive user interface. Using wagmi, RadzionKit, and TypeScript, we've implemented the commit-reveal pattern for secure name registration while keeping users informed with step-by-step progress tracking. The result is a robust application that makes ENS registration accessible without compromising on security.

Top comments (1)

Collapse
 
mehmetakar profile image
mehmet akar

Nice work! Did you make it completely or partially with Cursor? I wonder your Cursor experience...