How to update your ENS TXT records from your own application

How to update your ENS TXT records from your own application

There's something magic with ENS. In addition to being able to bind a username to a wallet address, like a domain name would be bound to an IP address, it also offers the possibility to attach metadata to your ENS name. The official name used for this is TXT records, a term borrowed from the DNS world, and they can be your email address, the URL of your website, your Twitter handle, your profile picture, and many other things.

This is how it looks.

What's magic about that? Well, as ENS is entirely decentralized, whenever you update your TXT records on ens.domains, your changes will be visible to all websites and applications that implemented ENS.

Below you can see my decentralized info on the Rainbow, Tap, and Eth.xyz websites.

It sounds silly, but when you change your name, bio, or profile picture on Twitter, it doesn't magically update on Facebook, LinkedIn, or TikTok. It's a bit like the principle of Gravatar, where you just have to upload your profile picture once and then it's available everywhere, but it goes a little further. Moreover, unlike Gravatar which is a centralized service, ENS is completely decentralized and is not likely to disappear overnight, unless Ethereum completely collapses.

OK, all that is nice but it's not extremely revolutionary I must admit. The real innovation for me lies in the fact that in addition to being able to read TXT records from anywhere, it is also possible to modify them from anywhere. Earlier, I explained that you had to go to the ens.domains website to be able to modify your TXT records, but in reality, this website is simply a frontend that interacts with the ENS smart contract that resides on the Ethereum blockchain. And like any smart contract that resides on the Ethereum blockchain, it is possible to access it both in reading and in writing from anywhere.

So, let's see how we can do that. ENS has several smart contracts deployed on Ethereum Mainnet but the one that interests us is called "ENS: Public Resolver 2" (with the address 0x4976fb03C32e5B8cfe2b6cCB31c09Ba78EBaBa41). I found it by just looking in MetaMask which contract ens.domains interacted with when I wanted to modify my TXT records.

All the write methods of the ENS contract.

This contract has multiple methods but to update our TXT records, we're only interested in two of them: setText and multicall.

setText is used to set a TXT record, as its name suggests. It takes three parameters: node (bytes32), key (string), and value (string). If I want to change my website TXT record, I just need to set key to "website" and value to the URL of my website, for example "https://vinch.be". The content you need to set for node is a bit less obvious.

As explained in this article, ENS works purely with fixed-length 256-bit cryptographic hashes, so you can't use your ENS name directly (like "vinchbat.eth" for example), but you need to derive the hash from it with a process called "namehash". The good news is that we don't have to implement this ourselves, we can just use the namehash function from ethers.js. For "vinchbat.eth", the hash is "0x3b58dd50ec6cc94ff0ddeb9e298e0051e8a1505b997b569e0160846775c1e794".

With wagmi, here's how you would do it in your React project:

import React from "react";
import { usePrepareContractWrite, useContractWrite } from "wagmi";
import { namehash } from "ethers/lib/utils";
import ensABI from "abi/ensPublicResolver2.json";

const node = namehash("vinchbat.eth");

const ENS = () => {
  const { config } = usePrepareContractWrite({
    addressOrName: "0x4976fb03c32e5b8cfe2b6ccb31c09ba78ebaba41",
    contractInterface: ensABI,
    functionName: "setText",
    args: [node, "url", "https://vinch.be"],
  });

  const { write } = useContractWrite({
    ...config,
  });

  return (
    <button
      onClick={() => {
        write?.();
      }}
    >
      Sign Transaction
    </button>
  );
};

export default ENS;

contractInterface is the ABI of the contract. The ABI is a descriptive interface of the contract, so we know how we can interact with it. There are many ways to get a contract ABI but the simplest is probably to get it from Etherscan. On the Etherscan contract page, go to the "Contract" tab, then "Code" and scroll until you find the "Contract ABI" section. On the right, click on "Export ABI" and choose "Raw/Text format". Save the content of the file to a JSON file that you import in your React code, the way we did in the code above.

And that's it! Well, that's only it if you want to update only one TXT record at a time. This is not ideal if you want to update several TXT records at once because you have to make a transaction and pay the gas fees for each TXT record individually. That's why multicall exists, it allows us to update multiple TXT records at once, in a single transaction.

multicall takes a single parameter data (an array of bytes). This array is actually an array of encoded function data, the function being in this case setText with its three parameters.

To better understand, it's probably best to directly look at the code for this:

import React from "react";
import { usePrepareContractWrite, useContractWrite } from "wagmi";
import { namehash, Interface } from "ethers/lib/utils";
import ensABI from "abi/ensPublicResolver2.json";

const RECORDS = [
  { key: "url", value: "https://vinch.be" },
  { key: "com.github", value: "vinch" },
  { key: "com.twitter", value: "vinchbat" },
];

const INTERFACE = new Interface(ensABI);

const data: Array<any> = [];
const node = namehash("vinchbat.eth");

RECORDS.forEach((record) => {
  const encodedFunctionData = INTERFACE.encodeFunctionData("setText", [
    node,
    record.key,
    record.value,
  ]);
  data.push(encodedFunctionData);
});

const ENS = () => {
  const { config } = usePrepareContractWrite({
    addressOrName: "0x4976fb03c32e5b8cfe2b6ccb31c09ba78ebaba41",
    contractInterface: ensABI,
    functionName: "multicall",
    args: [data],
  });

  const { write } = useContractWrite({
    ...config,
  });

  return (
    <button
      onClick={() => {
        write?.();
      }}
    >
      Sign Transaction
    </button>
  );
};

export default ENS;

Here, the most complex part to understand is the use of encodeFunctionData on INTERFACE but it looks more intimidating than it actually is as it will just encode the function name and its arguments as a hexadecimal string that can be used by the contract. It's all explained in ethers.js' documentation if you want to learn more about the encoding and decoding required to interact with contracts on the Ethereum network.

OK, so now you can update multiple TXT records at once on your own, so congratulations!

I could stop here but there's one more thing I want to talk about...

If you look at the very first screenshot of this article, you'll see a strange string for the avatar TXT record I set for vinchbat.eth: eip155:1/erc721:0x8efc99918af856699b53db659533396822f52941/166. This is actually a URI that refers to one of my JellyBots that you can find here on OpenSea.

Let's break down this URI to understand all of its parts:

  • eip155 is a reference to the EIP, the standard used for the formatting of this URI. It's unclear to me why EIP-155 was chosen for that, as it's an EIP focused on simple replay attack protection, but even the founder of ENS never bothered to give a simple explanation, so I guess it will just be a mystery forever. If you know why it is the way it is, let me know.
  • 1 is the chain identifier, a reference to the blockchain on which my JellyBot is located. 1 is Ethereum Mainnet but you find all the possible ids on the ChainList website (note that this is only for EVM-powered networks).
  • erc721 is the NFT standard used by the JellyBots collection. ERC-721 is the most common standard for NFTs, alongside ERC-1155.
  • 0x8efc99918af856699b53db659533396822f52941 is the contract address of the JellyBots NFT collection.
  • 166 is the token id of the JellyBot I want to use as my avatar.

That's it for today! Thanks for reading!