DEV Community

Cover image for Soroban Series 2: Building a Name Resolution Service
Shada Tosin
Shada Tosin

Posted on

Soroban Series 2: Building a Name Resolution Service

This is the second part of my Soroban Smart Contract Series. You can check out the first part here. 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.

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 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

#[derive(Clone, Debug)]
#[contracttype]
pub struct Record {
    pub owner: Address,
    pub resolver: Address,
    pub ttl: u32,
}
Enter fullscreen mode Exit fullscreen mode

We then created a trait for the Contract which will be implemented.

#[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;
}
Enter fullscreen mode Exit fullscreen mode

After creating the interface, we will begin building the core logic of the contract

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);
    }
Enter fullscreen mode Exit fullscreen mode

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.

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
    }
Enter fullscreen mode Exit fullscreen mode

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.

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))
}
Enter fullscreen mode Exit fullscreen mode

The Frontend

The frontend was put together with NextJS and Shadcn.

Top comments (0)