Soroban Series 2: Building a Name Resolution Service
Tags: soroban, rust, blockchain, domain
Published: 2023-08-28 21:57:12

This is the second part of my Soroban Smart Contract Series. You can check out the first part [here](https://dev.to/shada/soroban-series-1-building-a-payment-streaming-app-156b). I gave an overview of how to build a Decentralized Application in Soroban and also dropped some links to guide you in your journey.

Here is the link to the complete source code: [Github](https://github.com/TosinShada/tide-soroban-contract).

The Name service contract will be divided into three major contracts namely the registry, the registrar and the resolver.

## The Registry
This is the core contract that holds all the domain names for resolutions including the top-level domains. In this project we abstracted the interface of the project (thanks to the [Slender Project](https://github.com/eq-lab/Slender) for the inspiration) in order to seamlessly allow us do cross contract calls and test them.

All domain names in the registry are stored as a record shown below

``` Rust
#[derive(Clone, Debug)]
#[contracttype]
pub struct Record {
    pub owner: Address,
    pub resolver: Address,
    pub ttl: u32,
}
```
We then created a trait for the Contract which will be implemented.
``` Rust
#[contractspecfn(name = "Spec", export = false)]
#[contractclient(name = "SnsRegistryClient")]
pub trait SnsRegistryTrait {
    fn initialize(e: Env, admin: Address);
    fn set_record(
        e: Env,
        caller: Address,
        node: BytesN<32>,
        owner: Address,
        resolver: Address,
        ttl: u32,
    );
    fn set_owner(e: Env, caller: Address, node: BytesN<32>, owner: Address);
    fn set_subnode_owner(
        e: Env,
        caller: Address,
        node: BytesN<32>,
        label: BytesN<32>,
        owner: Address,
    );
    fn set_resolver(e: Env, caller: Address, node: BytesN<32>, resolver: Address);
    fn set_ttl(e: Env, caller: Address, node: BytesN<32>, ttl: u32);
    fn set_approval_for_all(e: Env, caller: Address, operator: Address, approved: bool);
    fn owner(e: Env, node: BytesN<32>) -> Address;
    fn resolver(e: Env, node: BytesN<32>) -> Address;
    fn ttl(e: Env, node: BytesN<32>) -> u32;
    fn record(e: Env, node: BytesN<32>) -> Record;
    fn record_exist(e: Env, node: BytesN<32>) -> bool;
    fn is_approved_for_all(e: Env, operator: Address, owner: Address) -> bool;
}
```
After creating the interface, we will begin building the core logic of the contract
``` Rust 
fn initialize(e: Env, admin: Address) {
        if has_administrator(&e) {
            panic!("already initialized")
        }
        set_administrator(&e, &admin);
    }

    fn set_record(
        e: Env,
        caller: Address,
        node: BytesN<32>,
        owner: Address,
        resolver: Address,
        ttl: u32,
    ) {
        caller.require_auth();
        require_node_authorised(&e, &node, &caller);
        set_parent_node_owner(&e, &node, &owner);
        set_resolver_ttl(&e, &node, &resolver, &ttl);
    }
```
As mentioned in the first series, the initialize function acts as the constructor and allows the contract deployer to set certain values that will be used over the lifetime of the contract.
You will notice in the above code that the Name is a BytesN<32> which is a special type in Soroban that can be used to represent a SHA256 hash.

## The Registrar
This is the contract that manages the registry and also registers and renews domains on its TLD (Top Level Domain). The major functions in this contract are shown below.

``` Rust
pub fn register(
        e: Env,
        caller: Address,
        owner: Address,
        name: BytesN<32>,
        duration: u64,
    ) -> u64 {
        caller.require_auth();
        // Comment out to allow anyone to register a name
        // require_active_controller(&e, &caller);
        require_registry_ownership(&e);
        assert!(is_name_available(&e, &name), "name is not available");

        // [todo] test this to see how it works
        let expiry_date = get_ledger_timestamp(&e) + duration;
        if expiry_date + GRACE_PERIOD > u64::MAX {
            panic!("duration is too long");
        }

        set_domain_owner(&e, &name, &owner);
        set_domain_expiry(&e, &name, &expiry_date);

        let base_node = get_base_node(&e);
        let registry = get_registry(&e);
        let registry_client = registry_contract::Client::new(&e, &registry);
        registry_client.set_subnode_owner(&e.current_contract_address(), &base_node, &name, &owner);

        expiry_date
    }

    pub fn renew(e: Env, caller: Address, name: BytesN<32>, duration: u64) -> u64 {
        caller.require_auth();
        require_active_controller(&e, &caller);
        require_registry_ownership(&e);
        assert!(!is_name_available(&e, &name), "name is not registered");

        let expiry_date = get_domain_expiry(&e, &name);
        // Check if the domain is expired or not registered by getting the expiry date which can either be a timestamp or 0
        // If the expiry date is 0 then the domain is not registered and therefore adding the grace period will certainly make it less than the current timestamp
        if expiry_date + GRACE_PERIOD < get_ledger_timestamp(&e) {
            panic!("domain is expired or not registered");
        }

        let new_expiry_date = expiry_date + duration;
        if new_expiry_date + GRACE_PERIOD > u64::MAX {
            panic!("duration is too long");
        }

        set_domain_expiry(&e, &name, &new_expiry_date);

        new_expiry_date
    }
```
From the above code, we can see that the contract will register and renew the domain based on availability and will give a certain number of days as grace period for the user to renew their subscription.

## The Resolver
The resolver holds the mapping of a domain name to an address as well as a collection of text records. Some of its core functions are shown below.

``` Rust
fn set_name(e: &Env, node: &BytesN<32>, name: &Address) {
    e.storage()
        .persistent()
        .set(&DataKey::Names(node.clone()), name);
    e.storage()
        .persistent()
        .bump(&DataKey::Names(node.clone()), BUMP_AMOUNT);
}

fn set_text(e: &Env, node: &BytesN<32>, text: &String) {
    let mut texts = get_text(&e, &node);
    texts.push_back(text.clone());
    e.storage()
        .persistent()
        .set(&DataKey::Texts(node.clone()), &texts);
    e.storage()
        .persistent()
        .bump(&DataKey::Texts(node.clone()), BUMP_AMOUNT);
}
fn get_name(e: &Env, node: &BytesN<32>) -> Address {
    e.storage()
        .persistent()
        .get::<_, Address>(&DataKey::Names(node.clone()))
        .expect("No name found")
}

fn get_text(e: &Env, node: &BytesN<32>) -> Vec<String> {
    e.storage()
        .persistent()
        .get::<_, Vec<String>>(&DataKey::Texts(node.clone()))
        .unwrap_or(Vec::new(&e))
}
```
## The Frontend
The frontend was put together with NextJS and Shadcn.