Build&Deploy Nameservice on Archway

Aleksandr Vorobev
6 min readMay 18, 2022

--

This is a guide on how to create a smart contract for a wallet naming service. Smart contract will be deployed on the Archway testnet and will be shown how you can interact with it.
This guide assumes that all the necessary dependencies for deploying smart contracts on the Arhcway network are already pre-installed on your computer. But if it’s not, then take a look at the “Getting Started” section of the official documentation and install whatever is required.
The final code is in my github repository, but let’s go in order.

Creating the Smart Contract

First of all, I create a project on the Archway testnet with the command:

archway new

The files in which we will write the code are located in the src folder.

First, let’s define the functionality of our service. We want the user to be able to create a name for their wallet, which they can then view. We also want to add the ability to transfer the created name to another wallet.

Let’s start with the msg.rs file in which we define the signatures of the future functions:

Let’s enter the initial message in which we will write the smart contract admin in order to be able to use it somehow in the future.

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct InstantiateMsg {
pub admin: String
}

Next, we will write messages for our future functions that will register and transfer names:

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ExecuteMsg {
Register { name: String },
Transfer { name: String, to: String },
}

Now that we’ve written messages for the contract state-modifying functions, we can now write messages for requests:

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum QueryMsg {
ResolveRecord { name: String },
Config {},
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct ResolveRecordResponse {
pub address: Option<String>,
}

The msg.rs file is ready, you can see its full code here

Now let’s move on to the state.rs file, which describes the state of the contract, that is, stores data.
We will store the config in state:

pub static CONFIG_KEY: &[u8] = b"config";#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct Config {
pub admin: String
}
pub fn config(storage: &mut dyn Storage) -> Singleton<Config> {
singleton(storage, CONFIG_KEY)
}
pub fn config_read(storage: &dyn Storage) -> ReadonlySingleton<Config> {
singleton_read(storage, CONFIG_KEY)
}

We will also store all name records:

pub static NAME_RESOLVER_KEY: &[u8] = b"nameresolver";#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct NameRecord {
pub owner: Addr,
}
pub fn resolver(storage: &mut dyn Storage) -> Bucket<NameRecord> {
bucket(storage, NAME_RESOLVER_KEY)
}
pub fn resolver_read(storage: &dyn Storage) -> ReadonlyBucket<NameRecord> {
bucket_read(storage, NAME_RESOLVER_KEY)
}

The state.rs file is ready, you can see its full code here

The next file will be error.rs in which we will describe all possible errors.

use cosmwasm_std::StdError;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum ContractError {
#[error("{0}")]
Std(#[from] StdError),
#[error("Unauthorized")]
Unauthorized {},
#[error("Name does not exist (name {name})")]
NameNotExists { name: String },
#[error("Name has been taken (name {name})")]
NameTaken { name: String },
#[error("Name too short (length {length} min_length {min_length})")]
NameTooShort { length: u64, min_length: u64 },
#[error("Name too long (length {length} min_length {max_length})")]
NameTooLong { length: u64, max_length: u64 },
#[error("Invalid character(char {c}")]
InvalidCharacter { c: char },
}

These custom errors are needed to get more readable errors, and this is quite important, because we will impose certain restrictions on the names.

Now let’s move on to the main and most meaningful part of the code — this is the contract.rs file. It contains all the logic when receiving certain messages.
When initializing the smart contract, we will add the admin to the config and save it.

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn instantiate(
deps: DepsMut,
_env: Env,
_info: MessageInfo,
msg: InstantiateMsg,
) -> Result<Response, StdError> {
let config_state = Config {
admin: deps.api.addr_validate(&msg.admin)?.into_string(),
};
config(deps.storage).save(&config_state)?;
Ok(Response::default())
}

First, let’s define helper functions that will check the correctness of the name (validate_name) and the correctness of the character in the name (invalid_char)

fn invalid_char(c: char) -> bool {
let is_valid = c.is_digit(10) || c.is_ascii_lowercase() || (c == '.' || c == '-' || c == '_');
!is_valid
}
// (3-64 lowercase ascii letters, numbers, or . - _)
fn validate_name(name: &str) -> Result<(), ContractError> {
let length = name.len() as u64;
if (name.len() as u64) < MIN_NAME_LENGTH {
Err(ContractError::NameTooShort {
length,
min_length: MIN_NAME_LENGTH,
})
} else if (name.len() as u64) > MAX_NAME_LENGTH {
Err(ContractError::NameTooLong {
length,
max_length: MAX_NAME_LENGTH,
})
} else {
match name.find(invalid_char) {
None => Ok(()),
Some(bytepos_invalid_char_start) => {
let c = name[bytepos_invalid_char_start..].chars().next().unwrap();
Err(ContractError::InvalidCharacter { c })
}
}
}
}

Now let’s go in order and start with the function for registering the name, in it we first check that the name meets the requirements using the validate_name function and then if there is no such name yet — save it in storage.

pub fn execute_register(
deps: DepsMut,
_env: Env,
info: MessageInfo,
name: String,
) -> Result<Response, ContractError> {
validate_name(&name)?;
let key = name.as_bytes();
let record = NameRecord { owner: info.sender };
if (resolver(deps.storage).may_load(key)?).is_some() {
return Err(ContractError::NameTaken { name });
}
resolver(deps.storage).save(key, &record)?;
Ok(Response::default())
}

Then let’s move on to the function of transferring the name from one wallet to another. In it, we first check the correctness of the new address (new_owner), then get the name record from the storage, and if the owner of the name is the one who called the function, then change the owner of the name to the new one.

pub fn execute_transfer(
deps: DepsMut,
_env: Env,
info: MessageInfo,
name: String,
to: String,
) -> Result<Response, ContractError> {
let new_owner = deps.api.addr_validate(&to)?;
let key = name.as_bytes();
resolver(deps.storage).update(key, |record| {
if let Some(mut record) = record {
if info.sender != record.owner {
return Err(ContractError::Unauthorized {});
}
record.owner = new_owner.clone();
Ok(record)
} else {
Err(ContractError::NameNotExists { name: name.clone() })
}
})?;
Ok(Response::default())
}

Let’s also write a get request to get the owner of the name:

fn query_resolver(deps: Deps, _env: Env, name: String) -> StdResult<Binary> {
let key = name.as_bytes();
let address = match resolver_read(deps.storage).may_load(key)? {
Some(record) => Some(String::from(&record.owner)),
None => None,
};
let resp = ResolveRecordResponse { address };
to_binary(&resp)
}

And in the end, we will write a fairly standard logic for the execute and query functions, in which we will determine which function to call in the future:

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn execute(
deps: DepsMut,
env: Env,
info: MessageInfo,
msg: ExecuteMsg,
) -> Result<Response, ContractError> {
match msg {
ExecuteMsg::Register { name } => execute_register(deps, env, info, name),
ExecuteMsg::Transfer { name, to } => execute_transfer(deps, env, info, name, to),
}
}
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult<Binary> {
match msg {
QueryMsg::ResolveRecord { name } => query_resolver(deps, env, name),
QueryMsg::Config {} => to_binary(&config_read(deps.storage).load()?),
}
}

The contract.rs file is ready, you can see its full code here

Instantiate the contract and check correctness

Let’s deploy a smart contract to the Archway network, specifying the admin wallet as an argument (in my case, this is archway1x86agw74qn0sery9k6rerthahe2ktrukvrkvc5)

archway deploy --args '{"admin":"archway1x86agw74qn0sery9k6rerthahe2ktrukvrkvc5"}'

After the contract is successfully deployed on the network, you need to remember its address (in my case it is archway1q5gt0quuj07yth7cnlp35p49pg22p2hyswdhpl22a2j8c0z74y0qv6yfer) and then you can check its performance.

To do this, first register a new name:

archway tx --args '{ "register": {"name": "alex"}}'

Now let’s check the owner of the name alex with a query:

archwayd query wasm contract-state smart archway1q5gt0quuj07yth7cnlp35p49pg22p2hyswdhpl22a2j8c0z74y0qv6yfer '{ "resolve_record": {"name": "alex" } }' --node https://rpc.constantine-1.archway.tech:443

This request will return the wallet from which the “register” function was called.

Now let’s transfer this name to another wallet using the “transfer” command:

archway tx --args '{ "transfer": {"name": "alex", "to": "archway148tmwcuw0fsf0vk75xp9r0h26y52hfmx0nwv05"}}'

After the command has successfully completed, we will run the query again with getting the owner of the name alex and now it should issue a new address: “archway148tmwcuw0fsf0vk75xp9r0h26y52hfmx0nwv05”

So everything works!

Full code(Github)

By f1rze#6069

--

--

No responses yet