Mint L1 Subnames

Guide on how to use Namespace backend API and Smart Contracts to integrate and mint a subname on Ethereum mainnet or a Test network.

In this tutorial, we will create a simple dapp using React, RainbowKit, and Viem.

The dapp will:

  1. Allow user to connect a wallet

  2. Search for mintable subnames

  3. Mint a subname

The code for the demo project can also be found on our GitHub.

If at any point you require some assistance or help implementing this, make sure to reach out to us in our Discord and we will answer all your questions.

Project setup

To start, we first need to initialize a blank React project.

yarn create-vite-app

Then, we would need to install RainbowKit and Viem so that we can connect our wallet and communicate with the Ethereum protocol.

yarn add @rainbow-me/rainbowkit wagmi [email protected] @tanstack/react-query

I will not go into details on how to configure Rainbowkit and get a Connect button, since RainbowKit already has pretty decent documentation: Raibowkit Docs

After setting up everything, we should see a nice-looking ConnectButton which allows us to connect our wallet.

Find mintable subnames

After we set up our project successfully, the next thing we need is to find mintable subnames under ENS names which are listed on the Namespace platform.

If you would like to list your own ENS name to test it while doing this tutorial, you can create a listing on a Sepolia network. Head over to https://app.namespace.tech and switch to Sepolia.

If you'd like to do it on the Mainnet, go to: https://app.namespace.tech, connect your wallet, and visit Account -> Manager.

Instructions on how to list your ENS Name and issue/sell Subnames can be found in the Manager page.

Namespace backend API exposes an endpoint that allows searching for mintable subnames. GET /api/v1/listings/availabe

We'll create a React hook, which will use this endpoint to fetch subname data.

First, we need to install a lib for making HTTP requests. I will use Axios but you are free to use whatever you prefer.

yarn add axios

Then we will create our types.

types.ts
import { Address } from "viem";

export type NetworkName = "mainnet" | "sepolia";

export interface GetListingsRequest {
    minterAddress?: Address
    parentLabel?: string
    subnameLabel: string
    network: string
    pageSize?: number
}

export interface ListingsResponse {
    parentLabel: string
    parentName: string
    parentNamehash: string
    subnameLabel: string
    mintPrice: number
    listingOwner: Address
}

export interface PaginatedResponse {
    items: ListingsResponse[]
    totalItems: number
}

Then a React hook will call Namespace API and return a list of available listings.

use-namespace-listings.tsx
import { useEffect, useState } from "react";
import {
  GetListingsRequest,
  ListingsResponse,
  PaginatedResponse,
} from "./types";
import axios from "axios";

interface NamespaceListingsState {
  items: ListingsResponse[];
  totalItems: number;
  isFetching: boolean;
}

const BACKEND_API = "https://api.namespace.tech";

export const useNamespaceListings = (request: GetListingsRequest) => {
  const [state, setState] = useState<NamespaceListingsState>({
    items: [],
    totalItems: 0,
    isFetching: true,
  });

  useEffect(() => {
    const fetchListings = async (request: GetListingsRequest) => {
      return axios
        .get<PaginatedResponse>(`${BACKEND_API}/api/v1/listings/available`, {
          params: request,
        })
        .then((response) => response.data);
    };

    setState({
      ...state,
      isFetching: true,
    });
    fetchListings(request)
      .then((apiResponse) => {
        setState({
          items: apiResponse.items,
          totalItems: apiResponse.totalItems,
          isFetching: false,
        });
      })
      .catch((err) => {
        console.error(err.response.data);
        setState({
          ...state,
          isFetching: false,
        });
      });
  }, [request.subnameLabel, request.parentLabel, request.network]);

  return state;
};

Now that we have our available listings, we can create a form that will show the user's available subnames for minting and allow him to search and select.

Since I don't like a plain UI, I will use Thorin components to make my UI look nicer. They have pretty good documentation for setup, so I will not copy the code here. Thorin Docs

We will then create s simple form. The form has two inputs, which are used for filtering the subnames based on parentLabel and subnameLabel. The form also allows the user to select subname, and if a subname is selected, a mint button will appear.

mint-subname-form.tsx
import { Card, Input, Button, Typography, Spinner } from "@ensdomains/thorin";
import { useAccount } from "wagmi";
import { ListingsResponse, NetworkName } from "../hooks/types";
import { useState } from "react";
import { useNamespaceListings } from "../hooks/use-namespace-listings";

const networkNames: Record<number, NetworkName> = {
  1: "mainnet",
  11155111: "sepolia",
};

export const MintForm = () => {
  const { chain, address } = useAccount();
  const [subnameLabel, setSubnameLabel] = useState("");
  const [parentLabel, setParentLabel] = useState("");
  const [selectedSubname, setSelectedSubname] = useState<ListingsResponse>();
  const networkName = networkNames[chain.id]; 
   
  const { items, isFetching } = useNamespaceListings({
    network: networkName,
    subnameLabel: subnameLabel,
    parentLabel: parentLabel,
    minterAddress: address,
    pageSize: 5,
  });

  const handleLabelInput = (value: string) => {
    setSubnameLabel(value);
  };

  const handleParentInput = (value: string) => {
    setParentLabel(value);
  };

  const handleSelectSubname = (data: ListingsResponse) => {
    setSelectedSubname(data);
  };

  return (
    <Card className="mint-form">
      <Input
        size="small"
        onChange={(e) => handleParentInput(e.target.value)}
        label="Parent name"
        value={parentLabel}
      ></Input>
      <Input
        size="small"
        onChange={(e) => handleLabelInput(e.target.value)}
        label="Subname label"
        value={subnameLabel}
      ></Input>
      <Card>
        {isFetching && <Spinner></Spinner>}
        {!isFetching &&
          items.map((item) => (
            <MintableSubname
              data={item}
              key={item.parentNamehash}
              onSelect={handleSelectSubname}
            />
          ))}
      </Card>
      {selectedSubname && (
        <Input
          label="Selected subname"
          value={`${selectedSubname.subnameLabel}.${selectedSubname.parentName}`}
        />
      )}
      {selectedSubname && <Button>Mint</Button>}
    </Card>
  );
};

const MintableSubname = ({
  data,
  onSelect,
}: {
  data: ListingsResponse;
  onSelect: (data: ListingsResponse) => void;
}) => {
  const { subnameLabel, parentName, mintPrice } = data;

  return (
    <div className="mint-form-subname" onClick={() => onSelect(data)}>
      <Typography>{`${subnameLabel}.${parentName}`}</Typography>
      <Typography>{`Price ${mintPrice} ETH`}</Typography>
    </div>
  );
};

The form will initially show the subnames as we change inputs, as soon as we click, the subname will be selected and the Mint button will appear.

Mint L1 Subname

Now that we know which subnames are available for minting, the last step is to mint an actual NTF on the blockchain.

Since the listing data is stored and verification is done offchain, the Namespace backend will have to validate whether a subname can be minted and provide the minter with the required mint parameters and a signature. Using these, the minter can call a smart contract and mint a subname NFT.

As stated, minting is done in two steps.

  1. Generate minting parameters on Namespace Backed API

  2. Call mint function on Namespace smart contract

We will now update our small project according to this information.

First, we will update our types.ts file to include a request and response interface for generating mint parameters

types.ts
export interface MintParamsRequest {
    subnameLabel: string
    parentLabel: string
    subnameOwner: Address,
    resolver?: Address,
    network: NetworkName
}

export interface MintParamsResponse {
    parameters: {
        subnameLabel: string;
        parentNode: string;
        resolver: string;
        subnameOwner: string;
        fuses: number;
        mintPrice: string;
        mintFee: string;
        expiry: number
        ttl: number
      };
      signature: string;
}

Then we will create a function that will call our backend API endpoint for generating minting parameters.

generate-mint-params.ts
import { MintParamsRequest, MintParamsResponse } from "./types";
import axios from "axios";

const BACKEND_API = "https://api.namespace.tech";

export const generateMintingParams = (
  request: MintParamsRequest
): Promise<MintParamsResponse> => {
  return axios
    .post<MintParamsResponse>(`${BACKEND_API}/api/v1/mint`, request)
    .then((res) => res.data);
};

Third and the final part we need, is to actually mint a subname by calling the mint function of the NamespaceMinter smart contract. Since contracts are verified on Etherscan, we can find the ABI there.

use-namespace-minter.tsx
import { useAccount, usePublicClient, useWalletClient } from "wagmi";
import { MintParamsResponse } from "./types";
import ABI from "./abi/namespace-minter-abi.json";

const minterAddresses = {
  1: "0x18cC184E630A8290e46082351ba66A209a0787ba",
  11155111: "0x2674E4FAe872780F01B99e109E67749B765703fB",
};

export const useNamespaceMinter = () => {
  const publicClient = usePublicClient();
  const { data: walletClient } = useWalletClient();
  const { chain, address } = useAccount();

  const mint = async (mintParams: MintParamsResponse) => {
    const { parameters, signature } = mintParams;
    const {
      expiry,
      fuses,
      mintFee,
      mintPrice,
      parentNode,
      resolver,
      subnameLabel,
      subnameOwner,
      ttl,
    } = parameters;

    const mintFeeWei = BigInt(mintFee);
    const mintPriceWei = BigInt(mintPrice);
    const totalPrice = mintFeeWei + mintPriceWei;

    const { request } = await publicClient.simulateContract({
      abi: ABI,
      functionName: "mint",
      address: minterAddresses[chain.id],
      account: address,
      args: [
        {
          expiry,
          fuses,
          parentNode,
          resolver,
          subnameLabel,
          subnameOwner,
          ttl,
          mintFee: mintFeeWei,
          mintPrice: mintPriceWei,
        },
        signature,
      ],
      value: totalPrice,
    });
    return walletClient.writeContract(request);
  };

  return {
    mint
  }
};

Now that we have all the react components, the last piece of the puzzle is to actually implement minting functionality in the minting form we have previously created so that when subname is selected and the mint button is clicked, the user can send a transaction and mint subname.

mint-subname-form.tsx
  const onMint = async () => {
    try {
      setMinting(true);
      const mintRequest: MintParamsRequest = {
        network: networkName,
        parentLabel: selectedSubname.parentLabel,
        label: selectedSubname.subnameLabel,
        subnameOwner: address,
      }
      const mintParams = await generateMintingParams(mintRequest);
      const tx = await mint(mintParams);
      console.log(tx)
    } catch(err) {
      console.log(err.response)
    } finally {
      setMinting(false);
    }
  }

After selecting a subname and clicking Mint, we should see.

The only thing left afterward is to wait for the transaction to finalize.

If you have any questions or feedback, feel free to join our Discord server and let us know!

Last updated