KNS Integration

Guide for implementing KNS Resolution

Basic Structure

For the basic structure, KNS referred to Ethereum Name Service(ENS).

  1. Registry contains information about the owner and the resolver.

  2. Resolver contains wallet addresses corresponding to each domain and additional information (email addresses, avatars, etc.)

The owner of the Registry is the holder of the domain NFT, but the result of resolving a domain can only be acquired by calling addr on a resolver. Please refer to the figure above. Hence, the owner of the Registry could be different from resolving a domain.

By default, a registered domain's resolver should be PublicResolver. PublicResolver has the addr method and can resolve to any appropriate address. Using KNS's front end, you can maintain always maintain the resolver as PublicResolver.

Forward Resolution (Domain Address)

This is an implementation of a forward resolution using ethers and eth-ens-namehash library.

const namehash = require("eth-ens-namehash");
const ethers = require("ethers");

/********************************************
    Declare provider and ABI to use here
********************************************/

async function forwardResolve(domain) {
    const EMPTY_ADDRESS = "0x0000000000000000000000000000000000000000";
    let registry = new ethers.Contract(
        REGISTRY_ADDRESS,
        REGISTRY_ABI,
        provider
    );
    let node = namehash.hash(domain);
    let resolverAddress = await registry.resolver(node);
    if (parseInt(resolverAddress, 16) === 0) {
        return EMPTY_ADDRESS;
    }
    let resolver = new ethers.Contract(
        resolverAddress, 
        PUBLIC_RESOLVER_ABI,
        provider
    );
    try {
        let address = await resolver.addr(node);
        return address;
    } catch(e) {
        console.error(e);
        return EMPTY_ADDRESS;
    }
}

forwardResolve("foo.klay").then(console.log);

On the contract, the domains are not managed as raw strings; They are managed as a namehash of the bytes32 format. The eth-ens-namehash library provides a function for calculating the namehash value. Of course, you can build it on your own using the link above.

Using the calculated namehash value, you first query a domain's resolver address to the contract Registry. If the returned resolver address is 0, it's either not a registered domain, or the resolver is set as 0. Since a forward resolution is impossible in this case, an empty address will be returned.

In other cases, use the returned resolver address and call addr method to obtain the address. There are possibilities where custom resolvers are set without the addr method, so we recommend setting an exception handling for such cases.

When a 0 address is returned, the returned value should be ignored.

const REGISTRY_ABI = ["function resolver(bytes32) view returns (address)"]
const PUBLIC_RESOLVER_ABI = ["function addr(bytes32) view returns (address)"]

The minimum ABI for a forward resolution is as follows:

const REGISTRY_ADDRESS = "0x0892ed3424851d2Bab4aC1091fA93C9851Eb5d7D"
const PUBLIC_RESOLVER_ADDRESS = "0xe2AE210c9b8601E00edE4aE5b9B23a80dCD12e3C"

Also, the above addresses are Registry and PublicResolver addresses deployed on the Cypress network. (The PublicResolver address is used later in this document.)

Event

To track changes in addresses, you can subscribe to the following event from the PublicResolver address. Of course, if the resolver is a custom resolver, tracking is not possible.

event AddrChanged(bytes32 indexed node, address addr);

_node_ is the namehash value of a domain, and this event means the address corresponding to the domain has been changed to _addr_.

When the owner or the resolver of a domain is changed, you should invalidate previously mapped addresses and query new addresses. For this, you should subscribe to the following events from the Registry address.

event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);

_tokenId_ is a simple casting value from bytes32 to unit256 for a namehash value of a domain, and this event means the owner of _tokenId_ has been change from _from_ to _to_. Registry follows IKIP17 standards, hence this event is same as a IKIP17 Transfer event.

event NewResolver(bytes32 indexed node, address resolver);

_node_ is the namehash value of a domain, and this event means the resolver address of the domain has been changed to _resolver_.

Taken together, the forward resolution of the version that manages the cache by subscribing to the events is represented in pseudocode in the following way:

// Forward resolution with caching
func forwardResolve(domain):
	namehash <- getNamehash(domain)
	if (namehash in cache):
		return cache[namehash]
	else:
		resolver <- getResolver(namehash)
		result <- resolver.addr(namehash)
		if (resolver == PUBLIC_RESOLVER):
			cache[namehash] <- result
		return result

// Daemon for listening events
func daemon():
	while (true):
		event <- listenEventFrom([REGISTRY, PUBLIC_RESOLVER])
		if (event.type == "AddrChanged"): // from PUBLIC_RESOLVER
			cache[event.node] <- event.addr
		elif (event.type == "Transfer"): // from REGISTRY
			invalidate cache[event.tokenId]
		elif (event.type == "NewResolver"): // from REGISTRY
			invalidate cache[event.node]

Reverse Resolution (Address → Domain)

The operation of reverse resolution is similar to how it works on ENS, so referring to ENS reverse resolution document could help your understanding.

Reverse resolution is a function where you can set a primary domain of an address among many domains that might have been mapped to it. For this, we use a unique domain named “addr.reverse”. The owner of “addr.reverse” domain in Registry is the ReverseRegistrar contract, and through this contract, users can acquire the ownership of the subdomain “(address).addr.reverse”.

For example, the address “0x314159265dd8dbb310642f98f50c066173c1259b”, can claim the ownership of the domain “314159265dd8dbb310642f98f50c066173c1259b.addr.reverse” through the ReverseRegistrar contract, and can set "hello.klay" as the name(“0x9f3f…17e6”) value and set its primary domain as "hello.klay" (“0x9f3f…17e6” is the namehash value of “3141…259b.addr.reverse”).

The resolver of “(address).addr.reverse” domain is set as DefaultReverseResolver by default, unlike other normal domains, and DefaultReverseResolver supports name(bytes32).

When setting up a primary domain, there could be cases where it is set to a domain that is not actually resolved to that address, or the ownership of a primary domain is changed. So you need to check some additional items when implement the reverse resolution, specifically:

  1. Query the resolver address of “(address).addr.reverse” to Registry. (When making the query, all the alphabets[a-f] should be lowercase, and 0x prefix should not be entered.) If the query result returns 0, it's a failed query. (Exit)

  2. Call name(bytes32 namehash) to the resolver as a result of process 1. The namehash is namehash value of “(address).addr.reverse”. If the query returns an empty string, it's a failed query. (Exit)

  3. Using the namehash of a domain as a result of process 2, query the resolver address to Registry. If the query result returns 0, it's a failed query. (Exit)

  4. Using the resolver address as a result of process 3, call addr(bytes32 namehash). The namehash is the namehash value of the domain resulting from process 2. If the address value returned is different from the address initially entered, it's a failed query. (Exit)

  5. If process from 1 to 4 was successful without any failure, the primary domain of the entered domain should be the domain resulting from process 2. If you encounterd a failure during the process, either the primary domain was not set, or there could be an error in its setting.

If we bring the example used above, for process 1, we will call resolver(“0x9f3f…17e6”) to Registry and get the resolver address corresponding to “3141…259b.addr.reverse”, which by default, will be DefaultReverseResolver address. For process 2, we will call name(“0x9f3f…17e6”) to that resolver address and get “hello.klay”. For process 3, we will use “0x6f37…b595”, which is the namehash value for “hello.klay”, and call resolver(“0x6f37…b595”) to the Registry and get the resolver address of “hello.klay”, which by default, will be PublicResolver address. For process 4, we will call addr(“0x6f37…b595”) to the actual resolver address and if it's resolved to “0x3141…259b” address, it's a success, and if not, it's a failure.

All of the processes above are implemented in the getName(address) function of the ReverseRecords contract. So, most simply, the reverse resolution could be implemented in the following way:

import "ethers";

async function reverseResolve(address: any) {
  let reverseRecords = new ethers.Contract(
    REVERSE_RECORDS_ADDRESS,
    REVERSE_RECORDS_ABI,
    provider
  );
  let name = await reverseRecords.getName(address);
  return name;
}

reverseResolve("0x314159265dd8dbb310642f98f50c066173c1259b").then(console.log);

The minimum ABI for a reverse resolution is as follows:

const REVERSE_RECORDS_ADDRESS = "0x87f4483E4157a6592dd1d1546f145B5EE22c790a"
const REVERSE_RECORDS_ABI = ["function getName(address) view returns (string)"]

Event

The execute caching on reverse resolution, you must subscribe to events. Basically, if any of the processes from 1 to 4 above has a different result, you should make a new query to get a new result. Among processes 1 through 4, processes 3 and 4 are verification processes to check that it returns the same address as a forward resolution for domains returned from process 2, so it is essential to subscribe to the events in the Forward Resolution.

Additionally, to track the change of the name (primary domain) set in DefaultReverseResolver during processes 1 and 2, you should subscribe to the following event:

event NameChanged(bytes32 indexed node, string name);

_node_ is the namehash value of “(address).addr.reverse” and this event means the name of the address was changed to _name_.

Last updated