From b7cfe31d72dd1b731dc46c94dcfb29ab716a1ea3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Thu, 4 May 2023 15:57:41 +0200 Subject: [PATCH] Initial version of nym-name-service contract (only) (#3380) * Initial version of nym-name-service, based on nym-service-provider-directory * rustfmt * Rename to NameEntry * Restrict address format * Remove deprecated random test * Fix clippy * Add to top-level Makefile * Restore wasm-opt Makefile rule * Restore NymAddress as enum * rustfmt * Add contract address to qa-qwerty.env * Rename NymAddress to Address * Tweak event output * rustfmt * add event_tag() * qwerty contract address --- Cargo.lock | 8 + Cargo.toml | 1 + Makefile | 2 + .../name-service/Cargo.toml | 11 + .../name-service/src/events.rs | 66 ++ .../name-service/src/lib.rs | 7 + .../name-service/src/msg.rs | 74 ++ .../name-service/src/response.rs | 70 ++ .../name-service/src/types.rs | 164 +++++ contracts/Cargo.lock | 210 +++++- contracts/Cargo.toml | 1 + contracts/name-service/Cargo.toml | 28 + contracts/name-service/build.rs | 13 + contracts/name-service/src/constants.rs | 16 + contracts/name-service/src/contract.rs | 272 +++++++ .../name-service/src/contract/execute.rs | 162 ++++ contracts/name-service/src/contract/query.rs | 68 ++ contracts/name-service/src/error.rs | 57 ++ .../name-service/src/integration_tests.rs | 445 +++++++++++ contracts/name-service/src/lib.rs | 57 ++ contracts/name-service/src/state/admin.rs | 14 + contracts/name-service/src/state/config.rs | 34 + contracts/name-service/src/state/mod.rs | 8 + .../name-service/src/state/name_id_counter.rs | 83 +++ contracts/name-service/src/state/names.rs | 697 ++++++++++++++++++ .../name-service/src/test_helpers/assert.rs | 65 ++ .../name-service/src/test_helpers/fixture.rs | 29 + .../name-service/src/test_helpers/helpers.rs | 107 +++ .../name-service/src/test_helpers/mod.rs | 4 + .../src/test_helpers/test_setup.rs | 153 ++++ envs/qa-qwerty.env | 1 + 31 files changed, 2916 insertions(+), 11 deletions(-) create mode 100644 common/cosmwasm-smart-contracts/name-service/Cargo.toml create mode 100644 common/cosmwasm-smart-contracts/name-service/src/events.rs create mode 100644 common/cosmwasm-smart-contracts/name-service/src/lib.rs create mode 100644 common/cosmwasm-smart-contracts/name-service/src/msg.rs create mode 100644 common/cosmwasm-smart-contracts/name-service/src/response.rs create mode 100644 common/cosmwasm-smart-contracts/name-service/src/types.rs create mode 100644 contracts/name-service/Cargo.toml create mode 100644 contracts/name-service/build.rs create mode 100644 contracts/name-service/src/constants.rs create mode 100644 contracts/name-service/src/contract.rs create mode 100644 contracts/name-service/src/contract/execute.rs create mode 100644 contracts/name-service/src/contract/query.rs create mode 100644 contracts/name-service/src/error.rs create mode 100644 contracts/name-service/src/integration_tests.rs create mode 100644 contracts/name-service/src/lib.rs create mode 100644 contracts/name-service/src/state/admin.rs create mode 100644 contracts/name-service/src/state/config.rs create mode 100644 contracts/name-service/src/state/mod.rs create mode 100644 contracts/name-service/src/state/name_id_counter.rs create mode 100644 contracts/name-service/src/state/names.rs create mode 100644 contracts/name-service/src/test_helpers/assert.rs create mode 100644 contracts/name-service/src/test_helpers/fixture.rs create mode 100644 contracts/name-service/src/test_helpers/helpers.rs create mode 100644 contracts/name-service/src/test_helpers/mod.rs create mode 100644 contracts/name-service/src/test_helpers/test_setup.rs diff --git a/Cargo.lock b/Cargo.lock index f0c3805124..bafb6428a1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3730,6 +3730,14 @@ dependencies = [ "thiserror", ] +[[package]] +name = "nym-name-service-common" +version = "0.1.0" +dependencies = [ + "cosmwasm-std", + "serde", +] + [[package]] name = "nym-network-defaults" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 61095a1b5c..4802f6e2d9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,7 @@ members = [ "common/cosmwasm-smart-contracts/group-contract", "common/cosmwasm-smart-contracts/mixnet-contract", "common/cosmwasm-smart-contracts/multisig-contract", + "common/cosmwasm-smart-contracts/name-service", "common/cosmwasm-smart-contracts/service-provider-directory", "common/cosmwasm-smart-contracts/vesting-contract", "common/credential-storage", diff --git a/Makefile b/Makefile index 728ed6588f..ad812f11b8 100644 --- a/Makefile +++ b/Makefile @@ -99,6 +99,7 @@ CONTRACTS_OUT_DIR=contracts/target/wasm32-unknown-unknown/release VESTING_CONTRACT=$(CONTRACTS_OUT_DIR)/vesting_contract.wasm MIXNET_CONTRACT=$(CONTRACTS_OUT_DIR)/mixnet_contract.wasm SERVICE_PROVIDER_DIRECTORY_CONTRACT=$(CONTRACTS_OUT_DIR)/nym_service_provider_directory.wasm +NAME_SERVICE_CONTRACT=$(CONTRACTS_OUT_DIR)/nym_name_service.wasm wasm: wasm-build wasm-opt @@ -109,6 +110,7 @@ wasm-opt: wasm-opt --disable-sign-ext -Os $(VESTING_CONTRACT) -o $(VESTING_CONTRACT) wasm-opt --disable-sign-ext -Os $(MIXNET_CONTRACT) -o $(MIXNET_CONTRACT) wasm-opt --disable-sign-ext -Os $(SERVICE_PROVIDER_DIRECTORY_CONTRACT) -o $(SERVICE_PROVIDER_DIRECTORY_CONTRACT) + wasm-opt --disable-sign-ext -Os $(NAME_SERVICE_CONTRACT) -o $(NAME_SERVICE_CONTRACT) # ----------------------------------------------------------------------------- # Misc diff --git a/common/cosmwasm-smart-contracts/name-service/Cargo.toml b/common/cosmwasm-smart-contracts/name-service/Cargo.toml new file mode 100644 index 0000000000..dac8fdb134 --- /dev/null +++ b/common/cosmwasm-smart-contracts/name-service/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "nym-name-service-common" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +cosmwasm-std = { workspace = true } +serde = { workspace = true, default-features = false, features = ["derive"] } +#nym-sphinx-addressing = { path = "../../nymsphinx/addressing" } diff --git a/common/cosmwasm-smart-contracts/name-service/src/events.rs b/common/cosmwasm-smart-contracts/name-service/src/events.rs new file mode 100644 index 0000000000..0454a8fb28 --- /dev/null +++ b/common/cosmwasm-smart-contracts/name-service/src/events.rs @@ -0,0 +1,66 @@ +use cosmwasm_std::{Coin, Event}; + +use crate::{NameId, RegisteredName}; + +pub enum NameEventType { + Register, + DeleteId, + DeleteName, + UpdateDepositRequired, +} + +impl std::fmt::Display for NameEventType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + NameEventType::Register => write!(f, "register"), + NameEventType::DeleteId => write!(f, "delete_id"), + NameEventType::DeleteName => write!(f, "delete_name"), + NameEventType::UpdateDepositRequired => write!(f, "update_deposit_required"), + } + } +} + +impl From for String { + fn from(event_type: NameEventType) -> Self { + event_type.to_string() + } +} + +pub const ACTION: &str = "action"; + +pub const NAME_ID: &str = "name_id"; +pub const NAME: &str = "name"; +pub const OWNER: &str = "owner"; + +pub const DEPOSIT_REQUIRED: &str = "deposit_required"; + +pub fn new_register_event(name_id: NameId, name: RegisteredName) -> Event { + Event::new(NameEventType::Register) + .add_attribute(ACTION, NameEventType::Register) + .add_attribute(NAME_ID, name_id.to_string()) + .add_attribute(NAME, name.name.to_string()) + .add_attribute(name.address.event_tag(), name.address.to_string()) + .add_attribute(OWNER, name.owner.to_string()) +} + +pub fn new_delete_id_event(name_id: NameId, name: RegisteredName) -> Event { + Event::new(NameEventType::DeleteId) + .add_attribute(ACTION, NameEventType::DeleteId) + .add_attribute(NAME_ID, name_id.to_string()) + .add_attribute(NAME, name.name.to_string()) + .add_attribute(name.address.event_tag(), name.address.to_string()) +} + +pub fn new_delete_name_event(name_id: NameId, name: RegisteredName) -> Event { + Event::new(NameEventType::DeleteId) + .add_attribute(ACTION, NameEventType::DeleteName) + .add_attribute(NAME_ID, name_id.to_string()) + .add_attribute(NAME, name.name.to_string()) + .add_attribute(name.address.event_tag(), name.address.to_string()) +} + +pub fn new_update_deposit_required_event(deposit_required: Coin) -> Event { + Event::new(NameEventType::UpdateDepositRequired) + .add_attribute(ACTION, NameEventType::UpdateDepositRequired) + .add_attribute(DEPOSIT_REQUIRED, deposit_required.to_string()) +} diff --git a/common/cosmwasm-smart-contracts/name-service/src/lib.rs b/common/cosmwasm-smart-contracts/name-service/src/lib.rs new file mode 100644 index 0000000000..f456db01aa --- /dev/null +++ b/common/cosmwasm-smart-contracts/name-service/src/lib.rs @@ -0,0 +1,7 @@ +pub mod events; +pub mod msg; +pub mod response; +pub mod types; + +// Re-export all types at the top-level +pub use types::*; diff --git a/common/cosmwasm-smart-contracts/name-service/src/msg.rs b/common/cosmwasm-smart-contracts/name-service/src/msg.rs new file mode 100644 index 0000000000..bbe86aa7ae --- /dev/null +++ b/common/cosmwasm-smart-contracts/name-service/src/msg.rs @@ -0,0 +1,74 @@ +use crate::{Address, NameId, NymName}; +use cosmwasm_std::Coin; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] +#[serde(rename_all = "snake_case")] +pub struct InstantiateMsg { + pub deposit_required: Coin, +} + +impl InstantiateMsg { + pub fn new(deposit_required: Coin) -> Self { + Self { deposit_required } + } +} + +#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] +#[serde(rename_all = "snake_case")] +pub struct MigrateMsg {} + +#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] +#[serde(rename_all = "snake_case")] +pub enum ExecuteMsg { + /// Announcing a name pointing to a nym-address + Register { name: NymName, address: Address }, + /// Delete a name entry by id + DeleteId { name_id: NameId }, + /// Delete a name entry by name + DeleteName { name: NymName }, + /// Change the deposit required for announcing a name + UpdateDepositRequired { deposit_required: Coin }, +} + +impl ExecuteMsg { + pub fn delete_id(name_id: NameId) -> Self { + ExecuteMsg::DeleteId { name_id } + } +} + +#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] +#[serde(rename_all = "snake_case")] +pub enum QueryMsg { + /// Query the name by it's assigned id + NameId { + name_id: NameId, + }, + // Query the names by the registrator + ByOwner { + owner: String, + }, + ByName { + name: NymName, + }, + ByAddress { + address: Address, + }, + All { + limit: Option, + start_after: Option, + }, + Config {}, + GetContractVersion {}, + #[serde(rename = "get_cw2_contract_version")] + GetCW2ContractVersion {}, +} + +impl QueryMsg { + pub fn all() -> QueryMsg { + QueryMsg::All { + limit: None, + start_after: None, + } + } +} diff --git a/common/cosmwasm-smart-contracts/name-service/src/response.rs b/common/cosmwasm-smart-contracts/name-service/src/response.rs new file mode 100644 index 0000000000..41c92cacd4 --- /dev/null +++ b/common/cosmwasm-smart-contracts/name-service/src/response.rs @@ -0,0 +1,70 @@ +use crate::{msg::ExecuteMsg, NameEntry, NameId, RegisteredName}; +use cosmwasm_std::Coin; +use serde::{Deserialize, Serialize}; + +/// Like [`NameEntry`] but since it's a response type the name is an option depending on if +/// the name exists or not. +#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] +#[serde(rename_all = "snake_case")] +pub struct NameEntryResponse { + pub name_id: NameId, + pub name: Option, +} + +#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] +#[serde(rename_all = "snake_case")] +pub struct NamesListResponse { + pub names: Vec, +} + +impl NamesListResponse { + pub fn new(names: Vec<(NameId, RegisteredName)>) -> NamesListResponse { + NamesListResponse { + names: names + .into_iter() + .map(|(name_id, name)| NameEntry::new(name_id, name)) + .collect(), + } + } +} + +#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] +#[serde(rename_all = "snake_case")] +pub struct PagedNamesListResponse { + pub names: Vec, + pub per_page: usize, + pub start_next_after: Option, +} + +impl PagedNamesListResponse { + pub fn new( + names: Vec<(NameId, RegisteredName)>, + per_page: usize, + start_next_after: Option, + ) -> PagedNamesListResponse { + let names = names + .into_iter() + .map(|(name_id, name)| NameEntry::new(name_id, name)) + .collect(); + PagedNamesListResponse { + names, + per_page, + start_next_after, + } + } +} + +#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] +#[serde(rename_all = "snake_case")] +pub struct ConfigResponse { + pub deposit_required: Coin, +} + +impl From for ExecuteMsg { + fn from(name: RegisteredName) -> Self { + ExecuteMsg::Register { + name: name.name, + address: name.address, + } + } +} diff --git a/common/cosmwasm-smart-contracts/name-service/src/types.rs b/common/cosmwasm-smart-contracts/name-service/src/types.rs new file mode 100644 index 0000000000..d8eec3a2ca --- /dev/null +++ b/common/cosmwasm-smart-contracts/name-service/src/types.rs @@ -0,0 +1,164 @@ +use std::fmt::{Display, Formatter}; + +use cosmwasm_std::{Addr, Coin}; +use serde::{Deserialize, Serialize}; + +/// The directory of services are indexed by [`ServiceId`]. +pub type NameId = u32; + +#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)] +pub struct RegisteredName { + /// The name pointing to the nym address + pub name: NymName, + /// The address of the service. + pub address: Address, + /// Service owner. + pub owner: Addr, + /// Block height at which the service was added. + pub block_height: u64, + /// The deposit used to announce the service. + pub deposit: Coin, +} + +/// String representation of a nym address, which is of the form +/// client_id.client_enc@gateway_id. +/// NOTE: entirely unvalidated. +#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)] +#[serde(rename_all = "snake_case")] +pub enum Address { + NymAddress(String), + // Possibly extension: + //Gateway(String) +} + +impl Address { + /// Create a new nym address. + pub fn new(address: &str) -> Self { + Self::NymAddress(address.to_string()) + } + + pub fn as_str(&self) -> &str { + match self { + Address::NymAddress(address) => address, + } + } + + pub fn event_tag(&self) -> &str { + match self { + Address::NymAddress(_) => "nym_address", + //Address::Gateway(_) => "gatway_address", + } + } +} + +impl Display for Address { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.as_str()) + } +} + +/// Name stored and pointing a to a nym-address +#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)] +#[serde(rename_all = "snake_case")] +pub struct NymName(String); + +#[derive(Debug)] +pub enum NymNameError { + InvalidName, +} + +fn is_valid_name_char(c: char) -> bool { + // Normal lowercase letters + (c.is_alphabetic() && c.is_lowercase()) + // or numbers + || c.is_numeric() + // special case hyphen or underscore + || c == '-' || c == '_' +} + +impl NymName { + pub fn new(name: &str) -> Result { + // We are a bit restrictive in which names we allow, to start out with. Consider relaxing + // this in the future. + if !name.chars().all(is_valid_name_char) { + return Err(NymNameError::InvalidName); + } + Ok(Self(name.to_string())) + } + + pub fn as_str(&self) -> &str { + &self.0 + } +} + +impl Display for NymName { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +/// [`RegisterdName`] together with the assigned [`NameId`]. +#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] +#[serde(rename_all = "snake_case")] +pub struct NameEntry { + pub name_id: NameId, + pub name: RegisteredName, +} + +impl NameEntry { + pub fn new(name_id: NameId, name: RegisteredName) -> Self { + Self { name_id, name } + } +} + +#[cfg(test)] +mod tests { + use super::NymName; + + #[test] + fn parse_nym_name() { + // Test some valid cases + assert!(NymName::new("foo").is_ok()); + assert!(NymName::new("foo-bar").is_ok()); + assert!(NymName::new("foo-bar-123").is_ok()); + assert!(NymName::new("foo_bar").is_ok()); + assert!(NymName::new("foo_bar_123").is_ok()); + + // And now test all some invalid ones + assert!(NymName::new("Foo").is_err()); + assert!(NymName::new("foo bar").is_err()); + assert!(NymName::new("foo!bar").is_err()); + assert!(NymName::new("foo#bar").is_err()); + assert!(NymName::new("foo$bar").is_err()); + assert!(NymName::new("foo%bar").is_err()); + assert!(NymName::new("foo&bar").is_err()); + assert!(NymName::new("foo'bar").is_err()); + assert!(NymName::new("foo(bar").is_err()); + assert!(NymName::new("foo)bar").is_err()); + assert!(NymName::new("foo*bar").is_err()); + assert!(NymName::new("foo+bar").is_err()); + assert!(NymName::new("foo,bar").is_err()); + assert!(NymName::new("foo.bar").is_err()); + assert!(NymName::new("foo.bar").is_err()); + assert!(NymName::new("foo/bar").is_err()); + assert!(NymName::new("foo/bar").is_err()); + assert!(NymName::new("foo:bar").is_err()); + assert!(NymName::new("foo;bar").is_err()); + assert!(NymName::new("foobar").is_err()); + assert!(NymName::new("foo?bar").is_err()); + assert!(NymName::new("foo@bar").is_err()); + assert!(NymName::new("fooBar").is_err()); + assert!(NymName::new("foo[bar").is_err()); + assert!(NymName::new("foo\"bar").is_err()); + assert!(NymName::new("foo\\bar").is_err()); + assert!(NymName::new("foo]bar").is_err()); + assert!(NymName::new("foo^bar").is_err()); + assert!(NymName::new("foo`bar").is_err()); + assert!(NymName::new("foo{bar").is_err()); + assert!(NymName::new("foo|bar").is_err()); + assert!(NymName::new("foo}bar").is_err()); + assert!(NymName::new("foo~bar").is_err()); + } +} diff --git a/contracts/Cargo.lock b/contracts/Cargo.lock index 265d666285..f260999dfb 100644 --- a/contracts/Cargo.lock +++ b/contracts/Cargo.lock @@ -536,7 +536,7 @@ checksum = "c762bae6dcaf24c4c84667b8579785430908723d5c889f469d76a41d59cc7a9d" dependencies = [ "curve25519-dalek", "ed25519", - "rand", + "rand 0.7.3", "serde", "sha2", "zeroize", @@ -662,6 +662,101 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8cbd1169bd7b4a0a20d92b9af7a7e0422888bd38a6f5ec29c1fd8c1558a272e" +[[package]] +name = "futures" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" + +[[package]] +name = "futures-executor" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" + +[[package]] +name = "futures-macro" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.12", +] + +[[package]] +name = "futures-sink" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" + +[[package]] +name = "futures-task" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" + +[[package]] +name = "futures-timer" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c" + +[[package]] +name = "futures-util" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + [[package]] name = "generic-array" version = "0.12.4" @@ -949,6 +1044,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + [[package]] name = "mixnet-vesting-integration-tests" version = "0.1.0" @@ -1051,7 +1152,7 @@ dependencies = [ "ed25519-dalek", "nym-pemstore", "nym-sphinx-types", - "rand", + "rand 0.7.3", "subtle-encoding", "thiserror", "x25519-dalek", @@ -1121,6 +1222,35 @@ dependencies = [ "thiserror", ] +[[package]] +name = "nym-name-service" +version = "0.1.0" +dependencies = [ + "anyhow", + "cosmwasm-std", + "cw-controllers", + "cw-multi-test", + "cw-storage-plus", + "cw-utils", + "cw2", + "nym-contracts-common", + "nym-name-service-common", + "rand 0.8.5", + "rstest", + "semver", + "serde", + "thiserror", + "vergen", +] + +[[package]] +name = "nym-name-service-common" +version = "0.1.0" +dependencies = [ + "cosmwasm-std", + "serde", +] + [[package]] name = "nym-pemstore" version = "0.2.0" @@ -1232,6 +1362,18 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" +[[package]] +name = "pin-project-lite" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + [[package]] name = "pkcs8" version = "0.8.0" @@ -1339,6 +1481,17 @@ dependencies = [ "rand_hc", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + [[package]] name = "rand_chacha" version = "0.2.2" @@ -1384,7 +1537,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c9e9532ada3929fb8b2e9dbe28d1e06c9b2cc65813f074fcb6bd5fbefeff9d56" dependencies = [ "num-traits", - "rand", + "rand 0.7.3", ] [[package]] @@ -1431,6 +1584,32 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rstest" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de1bb486a691878cd320c2f0d319ba91eeaa2e894066d8b5f8f117c000e9d962" +dependencies = [ + "futures", + "futures-timer", + "rstest_macros", + "rustc_version", +] + +[[package]] +name = "rstest_macros" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290ca1a1c8ca7edb7c3283bd44dc35dd54fdec6253a3912e201ba1072018fca8" +dependencies = [ + "cfg-if", + "proc-macro2", + "quote", + "rustc_version", + "syn 1.0.109", + "unicode-ident", +] + [[package]] name = "rustc_version" version = "0.4.0" @@ -1547,7 +1726,7 @@ checksum = "e801c1712f48475582b7696ac71e0ca34ebb30e09338425384269d9717c62cad" dependencies = [ "proc-macro2", "quote", - "syn 2.0.3", + "syn 2.0.12", ] [[package]] @@ -1563,9 +1742,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.94" +version = "1.0.96" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c533a59c9d8a93a09c6ab31f0fd5e5f4dd1b8fc9434804029839884765d04ea" +checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1" dependencies = [ "itoa", "ryu", @@ -1580,7 +1759,7 @@ checksum = "bcec881020c684085e55a25f7fd888954d56609ef363479dc5a1305eb0d40cab" dependencies = [ "proc-macro2", "quote", - "syn 2.0.3", + "syn 2.0.12", ] [[package]] @@ -1606,6 +1785,15 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "slab" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" +dependencies = [ + "autocfg", +] + [[package]] name = "sphinx-packet" version = "0.1.0" @@ -1624,7 +1812,7 @@ dependencies = [ "hmac", "lioness", "log", - "rand", + "rand 0.7.3", "rand_distr", "sha2", "subtle 2.4.1", @@ -1680,9 +1868,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.3" +version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8234ae35e70582bfa0f1fedffa6daa248e41dd045310b19800c4a36382c8f60" +checksum = "79d9531f94112cfc3e4c8f5f02cb2b58f72c97b7efd85f70203cc6d8efda5927" dependencies = [ "proc-macro2", "quote", @@ -1731,7 +1919,7 @@ checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.3", + "syn 2.0.12", ] [[package]] diff --git a/contracts/Cargo.toml b/contracts/Cargo.toml index aa19095158..9d1fb0ac6a 100644 --- a/contracts/Cargo.toml +++ b/contracts/Cargo.toml @@ -7,6 +7,7 @@ members = [ "mixnet-vesting-integration-tests", "multisig/cw3-flex-multisig", "multisig/cw4-group", + "name-service", "service-provider-directory", "vesting", ] diff --git a/contracts/name-service/Cargo.toml b/contracts/name-service/Cargo.toml new file mode 100644 index 0000000000..b5f56577f7 --- /dev/null +++ b/contracts/name-service/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "nym-name-service" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +cosmwasm-std = { workspace = true } +cw-controllers = { workspace = true } +cw-storage-plus = { workspace = true } +cw-utils = { workspace = true } +cw2 = { workspace = true } +nym-contracts-common = { path = "../../common/cosmwasm-smart-contracts/contracts-common", version = "0.4.0" } +nym-name-service-common = { path = "../../common/cosmwasm-smart-contracts/name-service" } +semver = { version = "1.0.16", default-features = false } +serde = { version = "1.0.155", default-features = false, features = ["derive"] } +thiserror = "1.0.39" + +[build-dependencies] +vergen = { version = "=7.4.3", default-features = false, features = ["build", "git", "rustc"] } + +[dev-dependencies] +anyhow = "1.0.40" +cw-multi-test = { workspace = true } +rand = "0.8.5" +rstest = "0.17.0" diff --git a/contracts/name-service/build.rs b/contracts/name-service/build.rs new file mode 100644 index 0000000000..bd26c886d2 --- /dev/null +++ b/contracts/name-service/build.rs @@ -0,0 +1,13 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use vergen::{vergen, Config}; + +fn main() { + let mut config = Config::default(); + if std::env::var("DOCS_RS").is_ok() { + // If we don't have access to git information, such as in a docs.rs build, don't error + *config.git_mut().skip_if_error_mut() = true; + } + vergen(config).expect("failed to extract build metadata"); +} diff --git a/contracts/name-service/src/constants.rs b/contracts/name-service/src/constants.rs new file mode 100644 index 0000000000..6ac0998ae0 --- /dev/null +++ b/contracts/name-service/src/constants.rs @@ -0,0 +1,16 @@ +// We limit the these for simplicity and to avoid having to deal with paging. +pub const MAX_NUMBER_OF_NAMES_PER_OWNER: u32 = 100; +pub const MAX_NUMBER_OF_NAMES_FOR_ADDRESS: u32 = 100; + +pub const NAME_DEFAULT_RETRIEVAL_LIMIT: u32 = 100; +pub const NAME_MAX_RETRIEVAL_LIMIT: u32 = 150; + +// Storage keys +pub const CONFIG_KEY: &str = "config"; +pub const ADMIN_KEY: &str = "admin"; +pub const NAME_ID_COUNTER_KEY: &str = "nidc"; + +pub const NAMES_PK_NAMESPACE: &str = "nanames"; +pub const NAMES_OWNER_IDX_NAMESPACE: &str = "naowner"; +pub const NAMES_ADDRESS_IDX_NAMESPACE: &str = "naaddress"; +pub const NAMES_NAME_IDX_NAMESPACE: &str = "naname"; diff --git a/contracts/name-service/src/contract.rs b/contracts/name-service/src/contract.rs new file mode 100644 index 0000000000..a88d831dc8 --- /dev/null +++ b/contracts/name-service/src/contract.rs @@ -0,0 +1,272 @@ +use crate::{ + error::{NameServiceError, Result}, + state::{self, Config}, +}; +use cosmwasm_std::{to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response}; +use nym_name_service_common::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; +use semver::Version; + +mod execute; +mod query; + +// version info for migration info +const CONTRACT_NAME: &str = "crate:nym-name-service"; +const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +pub fn instantiate( + mut deps: DepsMut<'_>, + _env: Env, + info: MessageInfo, + msg: InstantiateMsg, +) -> Result { + state::set_admin(deps.branch(), info.sender.clone())?; + + let config = Config { + deposit_required: msg.deposit_required, + }; + cw2::set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + state::save_config(deps.storage, &config)?; + + Ok(Response::new() + .add_attribute("method", "instantiate") + .add_attribute("admin", info.sender)) +} + +pub fn migrate( + deps: DepsMut<'_>, + _env: Env, + _msg: MigrateMsg, +) -> Result { + // Note: don't remove this particular bit of code as we have to ALWAYS check whether we have to + // update the stored version + let version: Version = CONTRACT_VERSION.parse().map_err(|error: semver::Error| { + NameServiceError::SemVerFailure { + value: CONTRACT_VERSION.to_string(), + error_message: error.to_string(), + } + })?; + + let storage_version_raw = cw2::get_contract_version(deps.storage)?.version; + let storage_version: Version = + storage_version_raw + .parse() + .map_err(|error: semver::Error| NameServiceError::SemVerFailure { + value: storage_version_raw, + error_message: error.to_string(), + })?; + + if storage_version < version { + cw2::set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + + // If state structure changed in any contract version in the way migration is needed, it + // should occur here, for example anything from `crate::queued_migrations::` + } + + Ok(Response::new()) +} + +pub fn execute( + deps: DepsMut<'_>, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + match msg { + ExecuteMsg::Register { name, address } => execute::register(deps, env, info, name, address), + ExecuteMsg::DeleteId { name_id } => execute::delete_id(deps, info, name_id), + ExecuteMsg::DeleteName { name } => execute::delete_name(deps, info, name), + ExecuteMsg::UpdateDepositRequired { deposit_required } => { + execute::update_deposit_required(deps, info, deposit_required) + } + } +} + +pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> Result { + let response = match msg { + QueryMsg::NameId { name_id } => to_binary(&query::query_id(deps, name_id)?), + QueryMsg::ByOwner { owner } => to_binary(&query::query_owner(deps, owner)?), + QueryMsg::ByAddress { address } => to_binary(&query::query_address(deps, address)?), + QueryMsg::ByName { name } => to_binary(&query::query_name(deps, name)?), + QueryMsg::All { limit, start_after } => { + to_binary(&query::query_all_paged(deps, limit, start_after)?) + } + QueryMsg::Config {} => to_binary(&query::query_config(deps)?), + QueryMsg::GetContractVersion {} => to_binary(&query::query_contract_version()), + QueryMsg::GetCW2ContractVersion {} => to_binary(&cw2::get_contract_version(deps.storage)?), + }; + Ok(response?) +} + +#[cfg(test)] +mod tests { + use super::*; + + use crate::test_helpers::{ + assert::{assert_config, assert_empty, assert_name, assert_names, assert_not_found}, + fixture::name_fixture, + helpers::{get_attribute, nyms}, + }; + + use cosmwasm_std::{ + testing::{mock_dependencies, mock_env, mock_info}, + Addr, Coin, + }; + use nym_name_service_common::{msg::ExecuteMsg, NameEntry, NameId}; + + const DENOM: &str = "unym"; + + #[test] + fn instantiate_contract() { + let mut deps = mock_dependencies(); + let msg = InstantiateMsg { + deposit_required: Coin::new(100u128, DENOM), + }; + let info = mock_info("creator", &[]); + let admin = info.sender.clone(); + + // Instantiate contract + let res = instantiate(deps.as_mut(), mock_env(), info, msg).unwrap(); + assert_eq!(res.messages.len(), 0); + + // Check that it worked by querying the config, and checking that the list of names is + // empty + assert_config(deps.as_ref(), &admin, Coin::new(100u128, DENOM)); + assert_empty(deps.as_ref()); + } + + #[test] + fn register_fails_incorrect_deposit() { + let mut deps = mock_dependencies(); + let msg = InstantiateMsg::new(nyms(100)); + let info = mock_info("creator", &[]); + let admin = info.sender.clone(); + let res = instantiate(deps.as_mut(), mock_env(), info, msg).unwrap(); + assert_eq!(res.messages.len(), 0); + + // Register + let msg: ExecuteMsg = name_fixture().into(); + let owner = name_fixture().owner.to_string(); + + assert_eq!( + execute( + deps.as_mut(), + mock_env(), + mock_info(&owner, &[nyms(99)]), + msg.clone() + ) + .unwrap_err(), + NameServiceError::InsufficientDeposit { + funds: 99u128.into(), + deposit_required: 100u128.into(), + } + ); + + assert_eq!( + execute( + deps.as_mut(), + mock_env(), + mock_info(&owner, &[nyms(101)]), + msg + ) + .unwrap_err(), + NameServiceError::TooLargeDeposit { + funds: 101u128.into(), + deposit_required: 100u128.into(), + } + ); + + assert_config(deps.as_ref(), &admin, Coin::new(100, DENOM)); + assert_empty(deps.as_ref()); + } + + #[test] + fn register_success() { + let mut deps = mock_dependencies(); + let msg = InstantiateMsg::new(nyms(100)); + let info = mock_info("creator", &[]); + let res = instantiate(deps.as_mut(), mock_env(), info, msg).unwrap(); + assert_eq!(res.messages.len(), 0); + + // Register + let msg: ExecuteMsg = name_fixture().into(); + let info = mock_info("steve", &[nyms(100)]); + let res = execute(deps.as_mut(), mock_env(), info, msg).unwrap(); + + // Check that the name has had name id assigned to it + let expected_id = 1; + let id: NameId = get_attribute(&res, "register", "name_id").parse().unwrap(); + assert_eq!(id, expected_id); + assert_eq!( + get_attribute(&res, "register", "name"), + "my-service".to_string() + ); + assert_eq!( + get_attribute(&res, "register", "nym_address"), + "client_id.client_key@gateway_id".to_string() + ); + + // The expected registered name + let expected_name = NameEntry { + name_id: expected_id, + name: name_fixture(), + }; + assert_names(deps.as_ref(), &[expected_name.clone()]); + assert_name(deps.as_ref(), &expected_name); + } + + #[test] + fn delete() { + let mut deps = mock_dependencies(); + let msg = InstantiateMsg::new(Coin::new(100, "unym")); + let info = mock_info("creator", &[]); + let res = instantiate(deps.as_mut(), mock_env(), info, msg).unwrap(); + assert_eq!(res.messages.len(), 0); + + // Register + let msg: ExecuteMsg = name_fixture().into(); + let info_steve = mock_info("steve", &[nyms(100)]); + assert_eq!(info_steve.sender, name_fixture().owner); + execute(deps.as_mut(), mock_env(), info_steve, msg).unwrap(); + + // The expected registerd name + let expected_id = 1; + let expected_name = NameEntry { + name_id: expected_id, + name: name_fixture(), + }; + assert_names(deps.as_ref(), &[expected_name]); + + // Removing someone else's name will fail + let msg = ExecuteMsg::delete_id(expected_id); + let info_timmy = mock_info("timmy", &[]); + assert_eq!( + execute(deps.as_mut(), mock_env(), info_timmy, msg).unwrap_err(), + NameServiceError::Unauthorized { + sender: Addr::unchecked("timmy") + } + ); + + // Removing an non-existent name will fail + let msg = ExecuteMsg::delete_id(expected_id + 1); + let info_owner = MessageInfo { + sender: name_fixture().owner, + funds: vec![], + }; + assert_eq!( + execute(deps.as_mut(), mock_env(), info_owner.clone(), msg).unwrap_err(), + NameServiceError::NotFound { + name_id: expected_id + 1 + } + ); + + // Remove as correct owner succeeds + let msg = ExecuteMsg::delete_id(expected_id); + let res = execute(deps.as_mut(), mock_env(), info_owner, msg).unwrap(); + assert_eq!( + get_attribute(&res, "delete_id", "name_id"), + expected_id.to_string() + ); + assert_names(deps.as_ref(), &[]); + assert_not_found(deps.as_ref(), expected_id); + } +} diff --git a/contracts/name-service/src/contract/execute.rs b/contracts/name-service/src/contract/execute.rs new file mode 100644 index 0000000000..a7396c1229 --- /dev/null +++ b/contracts/name-service/src/contract/execute.rs @@ -0,0 +1,162 @@ +use crate::{ + constants::{MAX_NUMBER_OF_NAMES_FOR_ADDRESS, MAX_NUMBER_OF_NAMES_PER_OWNER}, + error::{NameServiceError, Result}, + state, +}; +use cosmwasm_std::{Addr, BankMsg, Coin, Deps, DepsMut, Env, MessageInfo, Response, Uint128}; +use nym_name_service_common::{ + events::{ + new_delete_id_event, new_delete_name_event, new_register_event, + new_update_deposit_required_event, + }, + Address, NameId, NymName, RegisteredName, +}; + +use super::query; + +fn ensure_correct_deposit(will_deposit: Uint128, deposit_required: Uint128) -> Result<()> { + match will_deposit.cmp(&deposit_required) { + std::cmp::Ordering::Less => Err(NameServiceError::InsufficientDeposit { + funds: will_deposit, + deposit_required, + }), + std::cmp::Ordering::Equal => Ok(()), + std::cmp::Ordering::Greater => Err(NameServiceError::TooLargeDeposit { + funds: will_deposit, + deposit_required, + }), + } +} + +fn ensure_max_names_per_owner(deps: Deps, owner: Addr) -> Result<()> { + let current_entries = query::query_owner(deps, owner.to_string())?; + if current_entries.names.len() < MAX_NUMBER_OF_NAMES_PER_OWNER as usize { + Ok(()) + } else { + Err(NameServiceError::ReachedMaxNamesForOwner { + max_names: MAX_NUMBER_OF_NAMES_PER_OWNER, + owner, + }) + } +} + +fn ensure_max_names_per_address(deps: Deps, address: Address) -> Result<()> { + let current_entries = query::query_address(deps, address.clone())?; + if current_entries.names.len() < MAX_NUMBER_OF_NAMES_FOR_ADDRESS as usize { + Ok(()) + } else { + Err(NameServiceError::ReachedMaxNamesForAddress { + max_names: MAX_NUMBER_OF_NAMES_FOR_ADDRESS, + address, + }) + } +} + +fn ensure_name_exists(deps: Deps, name_id: NameId) -> Result<()> { + if state::names::has_name_id(deps.storage, name_id) { + Ok(()) + } else { + Err(NameServiceError::NotFound { name_id }) + } +} + +fn ensure_name_not_exists(deps: Deps, name: &NymName) -> Result<()> { + if state::names::has_name(deps.storage, name) { + println!("name already exists"); + Err(NameServiceError::NameAlreadyRegistered { name: name.clone() }) + } else { + Ok(()) + } +} + +fn ensure_sender_authorized(info: MessageInfo, names: &RegisteredName) -> Result<()> { + if info.sender == names.owner { + Ok(()) + } else { + Err(NameServiceError::Unauthorized { + sender: info.sender, + }) + } +} + +fn return_deposit(name_to_delete: &RegisteredName) -> BankMsg { + BankMsg::Send { + to_address: name_to_delete.owner.to_string(), + amount: vec![name_to_delete.deposit.clone()], + } +} + +/// Register a new name. It will be assigned a new name id. +pub fn register( + deps: DepsMut, + env: Env, + info: MessageInfo, + name: NymName, + address: Address, +) -> Result { + ensure_name_not_exists(deps.as_ref(), &name)?; + ensure_max_names_per_owner(deps.as_ref(), info.sender.clone())?; + ensure_max_names_per_address(deps.as_ref(), address.clone())?; + + let deposit_required = state::deposit_required(deps.storage)?; + let denom = deposit_required.denom.clone(); + let will_deposit = cw_utils::must_pay(&info, &denom) + .map_err(|err| NameServiceError::DepositRequired { source: err })?; + ensure_correct_deposit(will_deposit, deposit_required.amount)?; + + let new_name = RegisteredName { + address, + name, + owner: info.sender, + block_height: env.block.height, + deposit: Coin::new(will_deposit.u128(), denom), + }; + let name_id = state::names::save(deps.storage, &new_name)?; + + Ok(Response::new().add_event(new_register_event(name_id, new_name))) +} + +/// Delete an exsisting name. +pub fn delete_id(deps: DepsMut, info: MessageInfo, name_id: NameId) -> Result { + ensure_name_exists(deps.as_ref(), name_id)?; + let name_to_delete = state::names::load_id(deps.storage, name_id)?; + ensure_sender_authorized(info, &name_to_delete)?; + + state::names::remove_id(deps.storage, name_id)?; + let return_deposit_msg = return_deposit(&name_to_delete); + + Ok(Response::new() + .add_message(return_deposit_msg) + .add_event(new_delete_id_event(name_id, name_to_delete))) +} + +/// Delete an existing name by name. +pub(crate) fn delete_name(deps: DepsMut, info: MessageInfo, name: NymName) -> Result { + let name_to_delete = query::query_name(deps.as_ref(), name)?; + ensure_sender_authorized(info, &name_to_delete.name)?; + + state::names::remove_id(deps.storage, name_to_delete.name_id)?; + let return_deposit_msg = return_deposit(&name_to_delete.name); + + Ok(Response::new() + .add_message(return_deposit_msg) + .add_event(new_delete_name_event( + name_to_delete.name_id, + name_to_delete.name, + ))) +} + +/// Update the deposit required to register new names +pub(crate) fn update_deposit_required( + deps: DepsMut, + info: MessageInfo, + deposit_required: Coin, +) -> Result { + state::assert_admin(deps.as_ref(), &info.sender)?; + + let mut config = state::load_config(deps.storage)?; + config.deposit_required = deposit_required.clone(); + state::save_config(deps.storage, &config)?; + + Ok(Response::new().add_event(new_update_deposit_required_event(deposit_required))) +} diff --git a/contracts/name-service/src/contract/query.rs b/contracts/name-service/src/contract/query.rs new file mode 100644 index 0000000000..0ba8355e96 --- /dev/null +++ b/contracts/name-service/src/contract/query.rs @@ -0,0 +1,68 @@ +use cosmwasm_std::Deps; +use nym_contracts_common::ContractBuildInformation; +use nym_name_service_common::{ + response::{ConfigResponse, NamesListResponse, PagedNamesListResponse}, + Address, NameEntry, NameId, NymName, +}; + +use crate::{ + error::Result, + state::{self, names::PagedLoad}, +}; + +pub fn query_id(deps: Deps, name_id: NameId) -> Result { + let name = state::names::load_id(deps.storage, name_id)?; + Ok(NameEntry { name_id, name }) +} + +pub fn query_owner(deps: Deps, owner: String) -> Result { + let owner = deps.api.addr_validate(&owner)?; + let names = state::names::load_owner(deps.storage, owner)?; + Ok(NamesListResponse::new(names)) +} + +pub fn query_address(deps: Deps, address: Address) -> Result { + let names = state::names::load_address(deps.storage, &address)?; + Ok(NamesListResponse::new(names)) +} + +pub fn query_name(deps: Deps, name: NymName) -> Result { + state::names::load_name_entry(deps.storage, &name) + .map(|(name_id, name)| NameEntry::new(name_id, name)) +} + +pub fn query_all_paged( + deps: Deps, + limit: Option, + start_after: Option, +) -> Result { + let PagedLoad { + names, + limit, + start_next_after, + } = state::names::load_all_paged(deps.storage, limit, start_after)?; + Ok(PagedNamesListResponse::new(names, limit, start_next_after)) +} + +pub fn query_config(deps: Deps) -> Result { + let config = state::load_config(deps.storage)?; + Ok(config.into()) +} + +pub fn query_contract_version() -> ContractBuildInformation { + // as per docs + // env! macro will expand to the value of the named environment variable at + // compile time, yielding an expression of type `&'static str` + ContractBuildInformation { + build_timestamp: env!("VERGEN_BUILD_TIMESTAMP").to_string(), + build_version: env!("VERGEN_BUILD_SEMVER").to_string(), + commit_sha: option_env!("VERGEN_GIT_SHA").unwrap_or("NONE").to_string(), + commit_timestamp: option_env!("VERGEN_GIT_COMMIT_TIMESTAMP") + .unwrap_or("NONE") + .to_string(), + commit_branch: option_env!("VERGEN_GIT_BRANCH") + .unwrap_or("NONE") + .to_string(), + rustc_version: env!("VERGEN_RUSTC_SEMVER").to_string(), + } +} diff --git a/contracts/name-service/src/error.rs b/contracts/name-service/src/error.rs new file mode 100644 index 0000000000..66ca6485dd --- /dev/null +++ b/contracts/name-service/src/error.rs @@ -0,0 +1,57 @@ +use cosmwasm_std::{Addr, StdError}; +use cw_controllers::AdminError; +use nym_name_service_common::{Address, NameId, NymName}; +use thiserror::Error; + +#[derive(Error, Debug, PartialEq)] +pub enum NameServiceError { + #[error("{0}")] + Std(#[from] StdError), + + #[error("{0}")] + AdminError(#[from] AdminError), + + #[error("name id entry not found: {name_id}")] + NotFound { name_id: NameId }, + + #[error("name entry not found: {name}")] + NameNotFound { name: NymName }, + + #[error("{sender} is not registrator of name")] + Unauthorized { sender: Addr }, + + #[error("deposit required to register a name")] + DepositRequired { source: cw_utils::PaymentError }, + + #[error("insufficiant deposit: {funds}, required: {deposit_required}")] + InsufficientDeposit { + funds: cosmwasm_std::Uint128, + deposit_required: cosmwasm_std::Uint128, + }, + + #[error("deposit too large: {funds}, required: {deposit_required}")] + TooLargeDeposit { + funds: cosmwasm_std::Uint128, + deposit_required: cosmwasm_std::Uint128, + }, + + #[error("reached the max number of names ({max_names}) for owner {owner}")] + ReachedMaxNamesForOwner { max_names: u32, owner: Addr }, + + #[error("reached the max number of names ({max_names}) for address {0}", address.to_string())] + ReachedMaxNamesForAddress { max_names: u32, address: Address }, + + #[error("failed to parse {value} into a valid SemVer version: {error_message}")] + SemVerFailure { + value: String, + error_message: String, + }, + + #[error("duplicate entries detected for name: {name}")] + DuplicateNames { name: NymName }, + + #[error("name already registered: {name}")] + NameAlreadyRegistered { name: NymName }, +} + +pub(crate) type Result = std::result::Result; diff --git a/contracts/name-service/src/integration_tests.rs b/contracts/name-service/src/integration_tests.rs new file mode 100644 index 0000000000..b7e68ffdc8 --- /dev/null +++ b/contracts/name-service/src/integration_tests.rs @@ -0,0 +1,445 @@ +//! Integration tests using cw-multi-test. + +use cosmwasm_std::Addr; +use nym_name_service_common::{ + response::{ConfigResponse, PagedNamesListResponse}, + Address, NameEntry, NymName, RegisteredName, +}; + +use crate::{ + constants::NAME_DEFAULT_RETRIEVAL_LIMIT, + error::NameServiceError, + test_helpers::{fixture::name_entry, helpers::nyms, test_setup::TestSetup}, +}; + +#[test] +fn instantiate_contract() { + TestSetup::new(); +} + +#[test] +fn query_config() { + assert_eq!( + TestSetup::new().query_config(), + ConfigResponse { + deposit_required: nyms(100), + } + ); +} + +#[test] +fn register_and_query_name() { + let mut setup = TestSetup::new(); + assert_eq!( + setup.query_all(), + PagedNamesListResponse { + names: vec![], + per_page: NAME_DEFAULT_RETRIEVAL_LIMIT as usize, + start_next_after: None, + } + ); + + // Register a first name + let owner = Addr::unchecked("owner"); + let name = NymName::new("steves-server").unwrap(); + let nym_address = Address::new("nym-address"); + assert_eq!(setup.contract_balance(), nyms(0)); + assert_eq!(setup.balance(&owner), nyms(250)); + setup.register(name.clone(), nym_address.clone(), owner.clone()); + + // Deposit is deposited to contract and deducted from owners's balance + assert_eq!(setup.contract_balance(), nyms(100)); + assert_eq!(setup.balance(&owner), nyms(150)); + + // We can query the full name list + assert_eq!( + setup.query_all(), + PagedNamesListResponse { + names: vec![NameEntry { + name_id: 1, + name: RegisteredName { + address: nym_address.clone(), + name: name.clone(), + owner: owner.clone(), + block_height: 12345, + deposit: nyms(100), + }, + }], + per_page: NAME_DEFAULT_RETRIEVAL_LIMIT as usize, + start_next_after: Some(1), + } + ); + + // ... and we can query by id + assert_eq!( + setup.query_id(1), + NameEntry { + name_id: 1, + name: RegisteredName { + address: nym_address.clone(), + name: name.clone(), + owner: owner.clone(), + block_height: 12345, + deposit: nyms(100), + }, + } + ); + + // Register a second name + let owner2 = Addr::unchecked("owner2"); + let name2 = NymName::new("another_server").unwrap(); + let nym_address2 = Address::new("nymAddress2"); + setup.register(name2.clone(), nym_address2.clone(), owner2.clone()); + + assert_eq!(setup.contract_balance(), nyms(200)); + assert_eq!( + setup.query_all(), + PagedNamesListResponse { + names: vec![ + name_entry(1, name, nym_address, owner), + name_entry(2, name2, nym_address2, owner2) + ], + per_page: NAME_DEFAULT_RETRIEVAL_LIMIT as usize, + start_next_after: Some(2), + } + ); +} + +#[test] +fn cant_register_a_name_without_funds() { + let mut setup = TestSetup::new(); + assert_eq!(setup.contract_balance(), nyms(0)); + assert_eq!(setup.balance("owner"), nyms(250)); + setup.register( + NymName::new("my_name").unwrap(), + Address::new("nymAddress"), + Addr::unchecked("owner"), + ); + assert_eq!(setup.contract_balance(), nyms(100)); + assert_eq!(setup.balance("owner"), nyms(150)); + setup.register( + NymName::new("my_name2").unwrap(), + Address::new("nymAddress"), + Addr::unchecked("owner"), + ); + assert_eq!(setup.contract_balance(), nyms(200)); + assert_eq!(setup.balance("owner"), nyms(50)); + let res = setup + .try_register( + NymName::new("my_name3").unwrap(), + Address::new("nymAddress"), + Addr::unchecked("owner"), + ) + .unwrap_err(); + assert_eq!( + res.downcast::().unwrap(), + cosmwasm_std::StdError::Overflow { + source: cosmwasm_std::OverflowError::new( + cosmwasm_std::OverflowOperation::Sub, + "50", + "100" + ) + } + ); +} + +#[test] +fn delete_name() { + let mut setup = TestSetup::new(); + setup.register( + NymName::new("my_name").unwrap(), + Address::new("nymAddress"), + Addr::unchecked("owner"), + ); + assert_eq!(setup.contract_balance(), nyms(100)); + assert_eq!(setup.balance("owner"), nyms(150)); + assert!(!setup.query_all().names.is_empty()); + setup.delete(1, Addr::unchecked("owner")); + + // Deleting the name returns the deposit to the owner + assert_eq!(setup.contract_balance(), nyms(0)); + assert_eq!(setup.balance("owner"), nyms(250)); + assert!(setup.query_all().names.is_empty()); +} + +#[test] +fn only_owner_can_delete_name() { + let mut setup = TestSetup::new(); + assert_eq!(setup.contract_balance(), nyms(0)); + setup.register( + NymName::new("name").unwrap(), + Address::new("nymAddress"), + Addr::unchecked("owner"), + ); + assert_eq!(setup.contract_balance(), nyms(100)); + assert!(!setup.query_all().names.is_empty()); + + let delete_resp = setup + .try_delete(1, Addr::unchecked("not_owner")) + .unwrap_err(); + + assert_eq!(setup.contract_balance(), nyms(100)); + assert_eq!( + delete_resp.downcast::().unwrap(), + NameServiceError::Unauthorized { + sender: Addr::unchecked("not_owner") + } + ); +} + +#[test] +fn cant_delete_name_that_does_not_exist() { + let mut setup = TestSetup::new(); + setup.register( + NymName::new("foo").unwrap(), + Address::new("nymAddress"), + Addr::unchecked("owner"), + ); + assert_eq!(setup.contract_balance(), nyms(100)); + assert!(!setup.query_all().names.is_empty()); + + let delete_resp = setup.try_delete(0, Addr::unchecked("owner")).unwrap_err(); + assert_eq!(setup.contract_balance(), nyms(100)); + assert_eq!( + delete_resp.downcast::().unwrap(), + NameServiceError::NotFound { name_id: 0 } + ); + + let delete_resp = setup.try_delete(2, Addr::unchecked("owner")).unwrap_err(); + assert_eq!(setup.contract_balance(), nyms(100)); + assert_eq!( + delete_resp.downcast::().unwrap(), + NameServiceError::NotFound { name_id: 2 } + ); + + assert!(!setup.query_all().names.is_empty()); + setup.delete(1, Addr::unchecked("owner")); + assert_eq!(setup.contract_balance(), nyms(0)); + assert!(setup.query_all().names.is_empty()); +} + +#[test] +fn cant_register_the_same_name_multiple_times() { + let mut setup = TestSetup::new(); + + setup.register( + NymName::new("name").unwrap(), + Address::new("nymAddress"), + Addr::unchecked("owner"), + ); + let resp = setup + .try_register( + NymName::new("name").unwrap(), + Address::new("nymAddress"), + Addr::unchecked("owner"), + ) + .unwrap_err(); + + assert_eq!( + resp.downcast::().unwrap(), + NameServiceError::NameAlreadyRegistered { + name: NymName::new("name").unwrap() + } + ); +} + +#[test] +fn can_register_multiple_names_for_the_same_nym_address() { + let mut setup = TestSetup::new(); + let name1 = NymName::new("name1").unwrap(); + let name2 = NymName::new("name2").unwrap(); + let address = Address::new("nymaddress"); + let owner = Addr::unchecked("owner"); + + setup.register(name1.clone(), address.clone(), owner.clone()); + setup.register(name2.clone(), address.clone(), owner.clone()); + + assert_eq!( + setup.query_all().names, + vec![ + name_entry(1, name1, address.clone(), owner.clone()), + name_entry(2, name2, address, owner) + ], + ); +} + +#[test] +fn register_multiple_names_and_deleting_by_name() { + let mut setup = TestSetup::new(); + let owner1 = Addr::unchecked("wealthy_owner_1"); + let owner2 = Addr::unchecked("wealthy_owner_2"); + let nym_address1 = Address::new("nymaddress1"); + let nym_address2 = Address::new("nymaddress2"); + let name1 = NymName::new("name1").unwrap(); + let name2 = NymName::new("name2").unwrap(); + let name3 = NymName::new("name3").unwrap(); + let name4 = NymName::new("name4").unwrap(); + let name5 = NymName::new("name5").unwrap(); + + // We register the same address three times, but with different owners + assert_eq!(setup.contract_balance(), nyms(0)); + assert_eq!(setup.balance(&owner1), nyms(1000)); + setup.register(name1.clone(), nym_address1.clone(), owner1.clone()); + setup.register(name2.clone(), nym_address1.clone(), owner1.clone()); + setup.register(name3.clone(), nym_address2.clone(), owner1.clone()); + setup.register(name4.clone(), nym_address1.clone(), owner2.clone()); + setup.register(name5.clone(), nym_address2.clone(), owner2.clone()); + + assert_eq!(setup.contract_balance(), nyms(500)); + assert_eq!(setup.balance(&owner1), nyms(700)); + assert_eq!( + setup.query_all(), + PagedNamesListResponse { + names: vec![ + name_entry(1, name1.clone(), nym_address1.clone(), owner1.clone()), + name_entry(2, name2.clone(), nym_address1.clone(), owner1.clone()), + name_entry(3, name3.clone(), nym_address2.clone(), owner1.clone()), + name_entry(4, name4.clone(), nym_address1.clone(), owner2.clone()), + name_entry(5, name5.clone(), nym_address2.clone(), owner2.clone()), + ], + per_page: NAME_DEFAULT_RETRIEVAL_LIMIT as usize, + start_next_after: Some(5), + } + ); + + setup.delete_name(name1, owner1.clone()); + + assert_eq!(setup.contract_balance(), nyms(400)); + assert_eq!(setup.balance(&owner1), nyms(800)); + assert_eq!( + setup.query_all(), + PagedNamesListResponse { + names: vec![ + name_entry(2, name2, nym_address1.clone(), owner1.clone()), + name_entry(3, name3, nym_address2.clone(), owner1), + name_entry(4, name4, nym_address1, owner2.clone()), + name_entry(5, name5, nym_address2, owner2), + ], + per_page: NAME_DEFAULT_RETRIEVAL_LIMIT as usize, + start_next_after: Some(5), + } + ); +} + +#[test] +fn check_paging() { + let mut setup = TestSetup::new(); + let owner1 = Addr::unchecked("wealthy_owner_1"); + let owner2 = Addr::unchecked("wealthy_owner_2"); + let nym_address1 = Address::new("nymAddress1"); + let nym_address2 = Address::new("nymAddress2"); + let name1 = NymName::new("name1").unwrap(); + let name2 = NymName::new("name2").unwrap(); + let name3 = NymName::new("name3").unwrap(); + let name4 = NymName::new("name4").unwrap(); + let name5 = NymName::new("name5").unwrap(); + + // We register the same address three times, but with different owners + assert_eq!(setup.contract_balance(), nyms(0)); + assert_eq!(setup.balance(&owner1), nyms(1000)); + setup.register(name1.clone(), nym_address1.clone(), owner1.clone()); + setup.register(name2.clone(), nym_address1.clone(), owner1.clone()); + setup.register(name3.clone(), nym_address2.clone(), owner1.clone()); + setup.register(name4.clone(), nym_address1.clone(), owner2.clone()); + setup.register(name5.clone(), nym_address2.clone(), owner2.clone()); + + assert_eq!(setup.contract_balance(), nyms(500)); + assert_eq!(setup.balance(&owner1), nyms(700)); + assert_eq!( + setup.query_all(), + PagedNamesListResponse { + names: vec![ + name_entry(1, name1.clone(), nym_address1.clone(), owner1.clone()), + name_entry(2, name2.clone(), nym_address1.clone(), owner1.clone()), + name_entry(3, name3.clone(), nym_address2.clone(), owner1.clone()), + name_entry(4, name4.clone(), nym_address1.clone(), owner2.clone()), + name_entry(5, name5, nym_address2.clone(), owner2.clone()), + ], + per_page: NAME_DEFAULT_RETRIEVAL_LIMIT as usize, + start_next_after: Some(5), + } + ); + + setup.delete_name(name1, owner1.clone()); + + assert_eq!( + setup.query_all_with_limit(Some(2), None), + PagedNamesListResponse { + names: vec![ + name_entry(2, name2, nym_address1.clone(), owner1.clone()), + name_entry(3, name3.clone(), nym_address2.clone(), owner1.clone()), + ], + per_page: 2, + start_next_after: Some(3), + } + ); + + assert_eq!( + setup.query_all_with_limit(Some(2), Some(2)), + PagedNamesListResponse { + names: vec![ + name_entry(3, name3, nym_address2, owner1), + name_entry(4, name4, nym_address1, owner2), + ], + per_page: 2, + start_next_after: Some(4), + } + ); +} + +#[test] +fn name_id_is_not_resused_when_deleting_and_then_adding_a_new_names() { + let mut setup = TestSetup::new(); + setup.register( + NymName::new("myname1").unwrap(), + Address::new("nymAddress1"), + Addr::unchecked("owner1"), + ); + setup.register( + NymName::new("myname2").unwrap(), + Address::new("nymAddress2"), + Addr::unchecked("owner2"), + ); + setup.register( + NymName::new("myname3").unwrap(), + Address::new("nymAddress3"), + Addr::unchecked("owner3"), + ); + + setup.delete(1, Addr::unchecked("owner1")); + setup.delete(3, Addr::unchecked("owner3")); + + assert_eq!( + setup.query_all().names, + vec![name_entry( + 2, + NymName::new("myname2").unwrap(), + Address::new("nymAddress2"), + Addr::unchecked("owner2") + )] + ); + + setup.register( + NymName::new("myname4").unwrap(), + Address::new("nymAddress4"), + Addr::unchecked("owner4"), + ); + + assert_eq!( + setup.query_all().names, + vec![ + name_entry( + 2, + NymName::new("myname2").unwrap(), + Address::new("nymAddress2"), + Addr::unchecked("owner2") + ), + name_entry( + 4, + NymName::new("myname4").unwrap(), + Address::new("nymAddress4"), + Addr::unchecked("owner4") + ) + ] + ); +} diff --git a/contracts/name-service/src/lib.rs b/contracts/name-service/src/lib.rs new file mode 100644 index 0000000000..c2d5b6c5d6 --- /dev/null +++ b/contracts/name-service/src/lib.rs @@ -0,0 +1,57 @@ +//! The nym name service contract is for users to register names for the nym addresses. + +#![warn(clippy::expect_used)] +#![warn(clippy::unwrap_used)] + +use crate::error::Result; +use nym_name_service_common::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; + +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use cosmwasm_std::{Binary, Deps, DepsMut, Env, MessageInfo, Response}; +use error::NameServiceError; + +mod contract; +mod error; +mod state; + +pub mod constants; + +#[cfg(test)] +mod integration_tests; +#[cfg(test)] +mod test_helpers; + +/// Contract entry point for instantiation. +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + deps: DepsMut<'_>, + env: Env, + info: MessageInfo, + msg: InstantiateMsg, +) -> Result { + contract::instantiate(deps, env, info, msg) +} + +/// Contract entry point for migrations. +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn migrate(deps: DepsMut<'_>, env: Env, msg: MigrateMsg) -> Result { + contract::migrate(deps, env, msg) +} + +/// Contract entry point for execution +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut<'_>, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + contract::execute(deps, env, info, msg) +} + +/// Contract entry point for queries +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps<'_>, env: Env, msg: QueryMsg) -> Result { + contract::query(deps, env, msg) +} diff --git a/contracts/name-service/src/state/admin.rs b/contracts/name-service/src/state/admin.rs new file mode 100644 index 0000000000..e0ec915951 --- /dev/null +++ b/contracts/name-service/src/state/admin.rs @@ -0,0 +1,14 @@ +use cosmwasm_std::{Addr, Deps, DepsMut}; +use cw_controllers::Admin; + +use crate::{constants::ADMIN_KEY, error::Result}; + +const ADMIN: Admin = Admin::new(ADMIN_KEY); + +pub(crate) fn set_admin(deps: DepsMut<'_>, admin: Addr) -> Result<()> { + Ok(ADMIN.set(deps, Some(admin))?) +} + +pub(crate) fn assert_admin(deps: Deps, sender: &Addr) -> Result<()> { + Ok(ADMIN.assert_admin(deps, sender)?) +} diff --git a/contracts/name-service/src/state/config.rs b/contracts/name-service/src/state/config.rs new file mode 100644 index 0000000000..ed032e670c --- /dev/null +++ b/contracts/name-service/src/state/config.rs @@ -0,0 +1,34 @@ +use cosmwasm_std::{Coin, Storage}; +use cw_storage_plus::Item; +use nym_name_service_common::response::ConfigResponse; +use serde::{Deserialize, Serialize}; + +use crate::{constants::CONFIG_KEY, error::Result}; + +const CONFIG: Item = Item::new(CONFIG_KEY); + +#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)] +pub(crate) struct Config { + pub deposit_required: Coin, +} + +impl From for ConfigResponse { + fn from(config: Config) -> Self { + ConfigResponse { + deposit_required: config.deposit_required, + } + } +} + +pub(crate) fn save_config(store: &mut dyn Storage, config: &Config) -> Result<()> { + Ok(CONFIG.save(store, config)?) +} + +pub(crate) fn load_config(store: &dyn Storage) -> Result { + Ok(CONFIG.load(store)?) +} + +/// Return the deposit required to register a name. +pub(crate) fn deposit_required(store: &dyn Storage) -> Result { + Ok(CONFIG.load(store).map(|config| config.deposit_required)?) +} diff --git a/contracts/name-service/src/state/mod.rs b/contracts/name-service/src/state/mod.rs new file mode 100644 index 0000000000..4120ab91e8 --- /dev/null +++ b/contracts/name-service/src/state/mod.rs @@ -0,0 +1,8 @@ +pub mod admin; +pub mod config; +pub mod name_id_counter; +pub mod names; + +pub(crate) use admin::{assert_admin, set_admin}; +pub(crate) use config::{deposit_required, load_config, save_config, Config}; +pub(crate) use name_id_counter::next_name_id_counter; diff --git a/contracts/name-service/src/state/name_id_counter.rs b/contracts/name-service/src/state/name_id_counter.rs new file mode 100644 index 0000000000..bed384e7bb --- /dev/null +++ b/contracts/name-service/src/state/name_id_counter.rs @@ -0,0 +1,83 @@ +use cosmwasm_std::Storage; +use cw_storage_plus::Item; +use nym_name_service_common::NameId; + +use crate::{constants::NAME_ID_COUNTER_KEY, error::Result}; + +const NAME_ID_COUNTER: Item = Item::new(NAME_ID_COUNTER_KEY); + +/// Generate the next name id, store it and return it +pub(crate) fn next_name_id_counter(store: &mut dyn Storage) -> Result { + // The first id is 1. + let id = NAME_ID_COUNTER.may_load(store)?.unwrap_or_default() + 1; + NAME_ID_COUNTER.save(store, &id)?; + Ok(id) +} + +#[cfg(test)] +mod tests { + + use nym_name_service_common::NameEntry; + + use crate::test_helpers::{ + assert::assert_names, + fixture::name_fixture_name, + helpers::{delete_name_id, instantiate_test_contract, register_name}, + }; + + #[test] + fn get_next_name_id() { + let mut deps = instantiate_test_contract(); + + assert_eq!(register_name(deps.as_mut(), &name_fixture_name("foo")), 1); + assert_names( + deps.as_ref(), + &[NameEntry::new(1, name_fixture_name("foo"))], + ); + + assert_eq!(register_name(deps.as_mut(), &name_fixture_name("bar")), 2); + assert_eq!(register_name(deps.as_mut(), &name_fixture_name("baz")), 3); + assert_names( + deps.as_ref(), + &[ + NameEntry::new(1, name_fixture_name("foo")), + NameEntry::new(2, name_fixture_name("bar")), + NameEntry::new(3, name_fixture_name("baz")), + ], + ); + } + + #[test] + fn deleted_name_id_is_not_reused() { + let mut deps = instantiate_test_contract(); + + // Register two names + assert_eq!(register_name(deps.as_mut(), &name_fixture_name("one")), 1); + assert_eq!(register_name(deps.as_mut(), &name_fixture_name("two")), 2); + assert_names( + deps.as_ref(), + &[ + NameEntry::new(1, name_fixture_name("one")), + NameEntry::new(2, name_fixture_name("two")), + ], + ); + + // Delete the last entry + delete_name_id(deps.as_mut(), 2, "steve"); + assert_names( + deps.as_ref(), + &[NameEntry::new(1, name_fixture_name("one"))], + ); + + // Create a third entry. The index should not reuse the previous entry that we just + // deleted. + assert_eq!(register_name(deps.as_mut(), &name_fixture_name("two")), 3); + assert_names( + deps.as_ref(), + &[ + NameEntry::new(1, name_fixture_name("one")), + NameEntry::new(3, name_fixture_name("two")), + ], + ); + } +} diff --git a/contracts/name-service/src/state/names.rs b/contracts/name-service/src/state/names.rs new file mode 100644 index 0000000000..6fdd4181ad --- /dev/null +++ b/contracts/name-service/src/state/names.rs @@ -0,0 +1,697 @@ +use cosmwasm_std::{Addr, Order, StdError, StdResult, Storage}; +use cw_storage_plus::{Bound, Index, IndexList, IndexedMap, MultiIndex, UniqueIndex}; +use nym_name_service_common::{Address, NameId, NymName, RegisteredName}; + +use crate::{ + constants::{ + MAX_NUMBER_OF_NAMES_FOR_ADDRESS, MAX_NUMBER_OF_NAMES_PER_OWNER, + NAMES_ADDRESS_IDX_NAMESPACE, NAMES_NAME_IDX_NAMESPACE, NAMES_OWNER_IDX_NAMESPACE, + NAMES_PK_NAMESPACE, NAME_DEFAULT_RETRIEVAL_LIMIT, NAME_MAX_RETRIEVAL_LIMIT, + }, + error::{NameServiceError, Result}, +}; + +struct NameIndex<'a> { + // A name can only point to a single address + pub(crate) name: UniqueIndex<'a, String, RegisteredName, NameId>, + // An addresses can be pointed to by multiple names. + pub(crate) address: MultiIndex<'a, String, RegisteredName, NameId>, + pub(crate) owner: MultiIndex<'a, Addr, RegisteredName, NameId>, +} + +impl<'a> IndexList for NameIndex<'a> { + fn get_indexes(&'_ self) -> Box> + '_> { + let v: Vec<&dyn Index> = vec![&self.name, &self.address, &self.owner]; + Box::new(v.into_iter()) + } +} + +fn names<'a>() -> IndexedMap<'a, NameId, RegisteredName, NameIndex<'a>> { + let indexes = NameIndex { + name: UniqueIndex::new(|d| d.name.to_string(), NAMES_NAME_IDX_NAMESPACE), + address: MultiIndex::new( + |d| d.address.to_string(), + NAMES_PK_NAMESPACE, + NAMES_ADDRESS_IDX_NAMESPACE, + ), + owner: MultiIndex::new( + |d| d.owner.clone(), + NAMES_PK_NAMESPACE, + NAMES_OWNER_IDX_NAMESPACE, + ), + }; + IndexedMap::new(NAMES_PK_NAMESPACE, indexes) +} + +pub fn save(store: &mut dyn Storage, new_name: &RegisteredName) -> Result { + let name_id = super::next_name_id_counter(store)?; + names().save(store, name_id, new_name)?; + Ok(name_id) +} + +#[cfg(test)] +pub fn save_all(state: &mut dyn Storage, names: &[RegisteredName]) -> Result> { + let mut ids = vec![]; + for name in names { + ids.push(save(state, name)?); + } + Ok(ids) +} + +pub fn has_name_id(store: &dyn Storage, name_id: NameId) -> bool { + names().has(store, name_id) +} + +pub fn has_name(store: &dyn Storage, name: &NymName) -> bool { + load_name(store, name).is_ok() +} + +// Get the (key, name) entry for a given name +pub fn load_name_entry(store: &dyn Storage, name: &NymName) -> Result<(NameId, RegisteredName)> { + names() + .idx + .name + .range(store, None, None, Order::Ascending) + .find(|entry| { + if let Ok(entry) = entry { + &entry.1.name == name + } else { + false + } + }) + .ok_or(NameServiceError::NameNotFound { name: name.clone() })? + .map_err(NameServiceError::from) +} + +pub fn load_id(store: &dyn Storage, name_id: NameId) -> Result { + names().load(store, name_id).map_err(|err| match err { + StdError::NotFound { .. } => NameServiceError::NotFound { name_id }, + err => err.into(), + }) +} + +pub fn load_name(store: &dyn Storage, name: &NymName) -> Result { + names() + .idx + .name + .item(store, name.to_string())? + .map(|record| record.1) + .ok_or(NameServiceError::NameNotFound { name: name.clone() }) +} + +pub fn load_address( + store: &dyn Storage, + address: &Address, +) -> Result> { + names() + .idx + .address + .prefix(address.to_string()) + .range(store, None, None, Order::Ascending) + .take(MAX_NUMBER_OF_NAMES_FOR_ADDRESS as usize) + .collect::>>() + .map_err(NameServiceError::from) +} + +pub fn load_owner(store: &dyn Storage, owner: Addr) -> Result> { + names() + .idx + .owner + .prefix(owner) + .range(store, None, None, Order::Ascending) + .take(MAX_NUMBER_OF_NAMES_PER_OWNER as usize) + .collect::>>() + .map_err(NameServiceError::from) +} + +pub fn remove_id(store: &mut dyn Storage, name_id: NameId) -> Result<()> { + Ok(names().remove(store, name_id)?) +} + +#[cfg(test)] +pub fn remove_name(store: &mut dyn Storage, name: NymName) -> Result { + let name_info = load_name_entry(store, &name)?; + remove_id(store, name_info.0)?; + Ok(name_info.0) +} + +#[derive(Debug, PartialEq)] +pub struct PagedLoad { + pub names: Vec<(NameId, RegisteredName)>, + pub limit: usize, + pub start_next_after: Option, +} + +pub fn load_all_paged( + store: &dyn Storage, + limit: Option, + start_after: Option, +) -> Result { + let limit = limit + .unwrap_or(NAME_DEFAULT_RETRIEVAL_LIMIT) + .min(NAME_MAX_RETRIEVAL_LIMIT) as usize; + + let start = start_after.map(Bound::exclusive); + + let names = names() + .range(store, start, None, Order::Ascending) + .take(limit) + .collect::>>()?; + + let start_next_after = names.last().map(|name| name.0); + + Ok(PagedLoad { + names, + limit, + start_next_after, + }) +} + +#[cfg(test)] +mod tests { + use std::iter::zip; + + use cosmwasm_std::{ + testing::{MockApi, MockQuerier}, + MemoryStorage, OwnedDeps, + }; + use rstest::rstest; + + use crate::{ + error::NameServiceError, + test_helpers::{ + fixture::{name_fixture, name_fixture_full}, + helpers::instantiate_test_contract, + }, + }; + + use super::*; + + type TestDeps = OwnedDeps; + #[rstest::fixture] + fn deps() -> TestDeps { + instantiate_test_contract() + } + + #[rstest::fixture] + fn uniq_names() -> Vec { + vec![ + name_fixture_full("one", "address_one", "owner_one"), + name_fixture_full("two", "address_two", "owner_two"), + name_fixture_full("three", "address_three", "owner_three"), + ] + } + + #[rstest::fixture] + fn overlapping_addresses() -> Vec { + vec![ + name_fixture_full("one", "address_one", "owner_one"), + name_fixture_full("two", "address_two", "owner_two"), + name_fixture_full("three", "address_two", "owner_three"), + ] + } + + #[rstest::fixture] + fn overlapping_owners() -> Vec { + vec![ + name_fixture_full("one", "address_one", "owner_one"), + name_fixture_full("two", "address_two", "owner_two"), + name_fixture_full("three", "address_three", "owner_two"), + ] + } + + fn assert_not_registered(store: &dyn Storage, names: Vec, ids: Vec) { + let names: Vec<(NameId, RegisteredName)> = zip(ids, names).collect(); + let loaded = load_all_paged(store, None, None).unwrap(); + for (id, name) in &names { + assert!(!has_name_id(store, *id)); + assert!(!has_name(store, &name.name)); + assert!(!loaded.names.iter().any(|(i, _)| i == id)); + assert!(!loaded.names.iter().any(|(_, n)| n == name)); + } + } + + fn assert_registered(store: &dyn Storage, names: Vec, ids: Vec) { + assert!(names.len() == ids.len()); + let names: Vec<(NameId, RegisteredName)> = zip(ids, names).collect(); + let loaded = load_all_paged(store, None, None).unwrap(); + for (id, name) in &names { + assert!(has_name_id(store, *id)); + assert!(has_name(store, &name.name)); + assert!(loaded.names.iter().filter(|(i, _)| i == id).count() == 1); + assert!(loaded.names.iter().filter(|(_, n)| n == name).count() == 1); + } + } + + fn assert_only_these_registered( + store: &dyn Storage, + names: Vec, + ids: Vec, + ) { + let last_id = *ids.last().unwrap(); + let names: Vec<(NameId, RegisteredName)> = zip(ids, names).collect(); + for (id, name) in &names { + assert!(has_name_id(store, *id)); + assert!(has_name(store, &name.name)); + } + assert_eq!( + load_all_paged(store, None, None).unwrap(), + PagedLoad { + names, + limit: NAME_DEFAULT_RETRIEVAL_LIMIT as usize, + start_next_after: Some(last_id), + } + ) + } + + #[rstest] + fn single_basic_save_works(mut deps: TestDeps) { + save(deps.as_mut().storage, &name_fixture()).unwrap(); + } + + #[rstest] + fn save_same_name_twice_fails(mut deps: TestDeps) { + save(deps.as_mut().storage, &name_fixture()).unwrap(); + assert!(matches!( + save(deps.as_mut().storage, &name_fixture()).unwrap_err(), + NameServiceError::Std(StdError::GenericErr { .. }) + )); + } + + #[rstest] + fn has_name_works(mut deps: TestDeps) { + assert!(!has_name(&deps.storage, &name_fixture().name)); + save(deps.as_mut().storage, &name_fixture()).unwrap(); + assert!(has_name(&deps.storage, &name_fixture().name)); + } + + #[rstest] + fn has_name_id_works(mut deps: TestDeps) { + assert!(!has_name_id(&deps.storage, 1)); + save(deps.as_mut().storage, &name_fixture()).unwrap(); + assert!(has_name_id(&deps.storage, 1)); + } + + #[rstest] + fn has_name_id_with_incorrect_id_fails(mut deps: TestDeps) { + assert!(!has_name_id(&deps.storage, 2)); + save(deps.as_mut().storage, &name_fixture()).unwrap(); + assert!(!has_name_id(&deps.storage, 2)); + } + + #[rstest] + fn load_name_entry_works(mut deps: TestDeps) { + assert_eq!( + load_name_entry(deps.as_ref().storage, &name_fixture().name).unwrap_err(), + NameServiceError::NameNotFound { + name: name_fixture().name + } + ); + save(deps.as_mut().storage, &name_fixture()).unwrap(); + assert_eq!( + load_name_entry(deps.as_ref().storage, &name_fixture().name).unwrap(), + (1, name_fixture()) + ); + } + + #[rstest] + fn load_id_works(mut deps: TestDeps) { + assert_eq!( + load_id(deps.as_ref().storage, 1).unwrap_err(), + NameServiceError::NotFound { name_id: 1 } + ); + save(deps.as_mut().storage, &name_fixture()).unwrap(); + assert_eq!(load_id(deps.as_ref().storage, 1).unwrap(), name_fixture(),); + } + + #[rstest] + fn load_name_works(mut deps: TestDeps) { + assert_eq!( + load_name(deps.as_ref().storage, &name_fixture().name).unwrap_err(), + NameServiceError::NameNotFound { + name: name_fixture().name + } + ); + save(deps.as_mut().storage, &name_fixture()).unwrap(); + assert_eq!( + load_name(deps.as_ref().storage, &name_fixture().name).unwrap(), + name_fixture(), + ); + } + + #[rstest] + fn load_address_works(mut deps: TestDeps) { + assert_eq!( + load_address(deps.as_ref().storage, &name_fixture().address).unwrap(), + vec![], + ); + save(deps.as_mut().storage, &name_fixture()).unwrap(); + assert_eq!( + load_address(deps.as_ref().storage, &name_fixture().address).unwrap(), + vec![(1, name_fixture())], + ); + } + + #[rstest] + fn load_owner_works(mut deps: TestDeps) { + assert_eq!( + load_owner(deps.as_ref().storage, name_fixture().owner).unwrap(), + vec![], + ); + save(deps.as_mut().storage, &name_fixture()).unwrap(); + assert_eq!( + load_owner(deps.as_ref().storage, name_fixture().owner).unwrap(), + vec![(1, name_fixture())], + ); + } + + #[rstest] + fn load_all_paged_works(mut deps: TestDeps, uniq_names: Vec) { + assert_eq!( + load_all_paged(&deps.storage, None, None).unwrap(), + PagedLoad { + names: vec![], + limit: NAME_DEFAULT_RETRIEVAL_LIMIT as usize, + start_next_after: None + } + ); + save(deps.as_mut().storage, &uniq_names[0]).unwrap(); + assert_eq!( + load_all_paged(&deps.storage, None, None).unwrap(), + PagedLoad { + names: vec![(1, uniq_names[0].clone())], + limit: NAME_DEFAULT_RETRIEVAL_LIMIT as usize, + start_next_after: Some(1), + } + ); + } + + #[rstest] + fn remove_id_works(mut deps: TestDeps) { + save(deps.as_mut().storage, &name_fixture()).unwrap(); + assert!(has_name_id(&deps.storage, 1)); + assert!(has_name(&deps.storage, &name_fixture().name)); + remove_id(deps.as_mut().storage, 1).unwrap(); + assert!(!has_name_id(&deps.storage, 1)); + assert!(!has_name(&deps.storage, &name_fixture().name)); + } + + #[rstest] + fn remove_name_works(mut deps: TestDeps) { + save(deps.as_mut().storage, &name_fixture()).unwrap(); + assert!(has_name_id(&deps.storage, 1)); + assert!(has_name(&deps.storage, &name_fixture().name)); + remove_name(deps.as_mut().storage, name_fixture().name).unwrap(); + assert!(!has_name_id(&deps.storage, 1)); + assert!(!has_name(&deps.storage, &name_fixture().name)); + } + + #[rstest] + fn save_set_of_unique_names_works(mut deps: TestDeps, uniq_names: Vec) { + let num = uniq_names.len() as NameId; + let ids = (1..=num).collect::>(); + assert_not_registered(&deps.storage, uniq_names.clone(), ids.clone()); + let saved_ids = save_all(deps.as_mut().storage, &uniq_names).unwrap(); + assert_eq!(saved_ids, ids); + assert_registered(&deps.storage, uniq_names.clone(), ids.clone()); + assert_only_these_registered(&deps.storage, uniq_names, ids); + } + + #[rstest] + fn save_set_of_unique_names_generates_ids(mut deps: TestDeps, uniq_names: Vec) { + let num = uniq_names.len() as NameId; + let ids = save_all(deps.as_mut().storage, &uniq_names).unwrap(); + assert_eq!(ids, (1..=num).collect::>()); + } + + #[rstest] + fn load_name_entry_for_unique_set_works(mut deps: TestDeps, uniq_names: Vec) { + save_all(deps.as_mut().storage, &uniq_names).unwrap(); + for (id, name) in uniq_names.iter().enumerate() { + assert_eq!( + load_name_entry(deps.as_ref().storage, &name.name).unwrap(), + (id as NameId + 1, name.clone()), + ); + } + } + + #[rstest] + fn load_name_for_unique_set_works(mut deps: TestDeps, uniq_names: Vec) { + save_all(deps.as_mut().storage, &uniq_names).unwrap(); + for name in uniq_names { + assert_eq!( + load_name(deps.as_ref().storage, &name.name).unwrap(), + name.clone(), + ); + } + } + + #[rstest] + fn save_and_remove_name_id_from_set_of_unique_set_works( + mut deps: TestDeps, + uniq_names: Vec, + ) { + let ids = save_all(deps.as_mut().storage, &uniq_names).unwrap(); + remove_id(deps.as_mut().storage, ids[1]).unwrap(); + assert_eq!( + load_all_paged(deps.as_ref().storage, None, None).unwrap(), + PagedLoad { + names: vec![ + (1, name_fixture_full("one", "address_one", "owner_one")), + ( + 3, + name_fixture_full("three", "address_three", "owner_three") + ), + ], + limit: NAME_DEFAULT_RETRIEVAL_LIMIT as usize, + start_next_after: Some(3), + } + ); + } + + #[rstest] + fn save_and_remove_name_from_set_of_unique_set_works( + mut deps: TestDeps, + uniq_names: Vec, + ) { + save_all(deps.as_mut().storage, &uniq_names).unwrap(); + remove_name(deps.as_mut().storage, uniq_names[1].name.clone()).unwrap(); + assert_eq!( + load_all_paged(deps.as_ref().storage, None, None).unwrap(), + PagedLoad { + names: vec![ + (1, name_fixture_full("one", "address_one", "owner_one")), + ( + 3, + name_fixture_full("three", "address_three", "owner_three") + ), + ], + limit: NAME_DEFAULT_RETRIEVAL_LIMIT as usize, + start_next_after: Some(3), + } + ); + } + + #[rstest] + fn save_set_of_overlapping_addressed_works( + mut deps: TestDeps, + overlapping_addresses: Vec, + ) { + let ids = save_all(deps.as_mut().storage, &overlapping_addresses).unwrap(); + assert_registered(&deps.storage, overlapping_addresses.clone(), ids.clone()); + assert_only_these_registered(&deps.storage, overlapping_addresses, ids); + } + + #[rstest] + fn load_address_with_overlapping_addresses_works( + mut deps: TestDeps, + overlapping_addresses: Vec, + ) { + save_all(deps.as_mut().storage, &overlapping_addresses).unwrap(); + assert_eq!( + load_all_paged(deps.as_ref().storage, None, None).unwrap(), + PagedLoad { + names: vec![ + (1, name_fixture_full("one", "address_one", "owner_one")), + (2, name_fixture_full("two", "address_two", "owner_two")), + (3, name_fixture_full("three", "address_two", "owner_three")), + ], + limit: NAME_DEFAULT_RETRIEVAL_LIMIT as usize, + start_next_after: Some(3), + } + ); + assert_eq!( + load_address(deps.as_ref().storage, &Address::new("address_two")).unwrap(), + vec![ + (2, name_fixture_full("two", "address_two", "owner_two")), + (3, name_fixture_full("three", "address_two", "owner_three")), + ] + ); + } + + #[rstest] + fn save_and_remove_name_id_from_set_of_overlapping_addresses_works( + mut deps: TestDeps, + overlapping_addresses: Vec, + ) { + let ids = save_all(deps.as_mut().storage, &overlapping_addresses).unwrap(); + remove_id(deps.as_mut().storage, ids[1]).unwrap(); + assert_eq!( + load_all_paged(deps.as_ref().storage, None, None).unwrap(), + PagedLoad { + names: vec![ + (1, name_fixture_full("one", "address_one", "owner_one")), + (3, name_fixture_full("three", "address_two", "owner_three")), + ], + limit: NAME_DEFAULT_RETRIEVAL_LIMIT as usize, + start_next_after: Some(3), + } + ); + } + + #[rstest] + fn save_and_remove_name_from_set_of_overlapping_addresses_works( + mut deps: TestDeps, + overlapping_addresses: Vec, + ) { + save_all(deps.as_mut().storage, &overlapping_addresses).unwrap(); + remove_name(deps.as_mut().storage, overlapping_addresses[1].name.clone()).unwrap(); + assert_eq!( + load_all_paged(deps.as_ref().storage, None, None).unwrap(), + PagedLoad { + names: vec![ + (1, name_fixture_full("one", "address_one", "owner_one")), + (3, name_fixture_full("three", "address_two", "owner_three")), + ], + limit: NAME_DEFAULT_RETRIEVAL_LIMIT as usize, + start_next_after: Some(3), + } + ); + } + + #[rstest] + fn save_set_of_overlapping_owners_works( + mut deps: TestDeps, + overlapping_owners: Vec, + ) { + let ids = save_all(deps.as_mut().storage, &overlapping_owners).unwrap(); + assert_registered(&deps.storage, overlapping_owners.clone(), ids.clone()); + assert_only_these_registered(&deps.storage, overlapping_owners, ids); + } + + #[rstest] + fn load_owner_with_overlapping_owners_works( + mut deps: TestDeps, + overlapping_owners: Vec, + ) { + save_all(deps.as_mut().storage, &overlapping_owners).unwrap(); + assert_eq!( + load_all_paged(deps.as_ref().storage, None, None).unwrap(), + PagedLoad { + names: vec![ + (1, name_fixture_full("one", "address_one", "owner_one")), + (2, name_fixture_full("two", "address_two", "owner_two")), + (3, name_fixture_full("three", "address_three", "owner_two")), + ], + limit: NAME_DEFAULT_RETRIEVAL_LIMIT as usize, + start_next_after: Some(3), + } + ); + assert_eq!( + load_owner(deps.as_ref().storage, Addr::unchecked("owner_two")).unwrap(), + vec![ + (2, name_fixture_full("two", "address_two", "owner_two")), + (3, name_fixture_full("three", "address_three", "owner_two")), + ] + ); + } + + #[rstest] + fn save_and_remove_name_id_from_set_of_overlapping_owners_works( + mut deps: TestDeps, + overlapping_owners: Vec, + ) { + let ids = save_all(deps.as_mut().storage, &overlapping_owners).unwrap(); + assert_registered(&deps.storage, overlapping_owners.clone(), ids.clone()); + assert_only_these_registered(&deps.storage, overlapping_owners, ids); + remove_id(deps.as_mut().storage, 2).unwrap(); + assert_eq!( + load_all_paged(deps.as_ref().storage, None, None).unwrap(), + PagedLoad { + names: vec![ + (1, name_fixture_full("one", "address_one", "owner_one")), + (3, name_fixture_full("three", "address_three", "owner_two")), + ], + limit: NAME_DEFAULT_RETRIEVAL_LIMIT as usize, + start_next_after: Some(3), + } + ); + } + + #[rstest] + fn save_and_remove_name_from_set_of_overlapping_owners_works( + mut deps: TestDeps, + overlapping_owners: Vec, + ) { + let ids = save_all(deps.as_mut().storage, &overlapping_owners).unwrap(); + assert_registered(&deps.storage, overlapping_owners.clone(), ids.clone()); + assert_only_these_registered(&deps.storage, overlapping_owners.clone(), ids); + remove_name(deps.as_mut().storage, overlapping_owners[1].name.clone()).unwrap(); + assert_eq!( + load_all_paged(deps.as_ref().storage, None, None).unwrap(), + PagedLoad { + names: vec![ + (1, name_fixture_full("one", "address_one", "owner_one")), + (3, name_fixture_full("three", "address_three", "owner_two")), + ], + limit: NAME_DEFAULT_RETRIEVAL_LIMIT as usize, + start_next_after: Some(3), + } + ); + } + + #[rstest] + fn load_all_paged_with_limit_works(mut deps: TestDeps, uniq_names: Vec) { + save_all(deps.as_mut().storage, &uniq_names).unwrap(); + assert_eq!( + load_all_paged(&deps.storage, Some(2), None).unwrap(), + PagedLoad { + names: vec![ + (1, name_fixture_full("one", "address_one", "owner_one")), + (2, name_fixture_full("two", "address_two", "owner_two")), + ], + limit: 2, + start_next_after: Some(2), + } + ); + assert_eq!( + load_all_paged(&deps.storage, Some(1), Some(2)).unwrap(), + PagedLoad { + names: vec![( + 3, + name_fixture_full("three", "address_three", "owner_three") + ),], + limit: 1, + start_next_after: Some(3), + } + ); + assert_eq!( + load_all_paged(&deps.storage, Some(2), Some(2)).unwrap(), + PagedLoad { + names: vec![( + 3, + name_fixture_full("three", "address_three", "owner_three") + ),], + limit: 2, + start_next_after: Some(3), + } + ); + } + + #[test] + #[ignore] + fn max_page_limit_is_applied() { + // WIP(JON) + todo!(); + } +} diff --git a/contracts/name-service/src/test_helpers/assert.rs b/contracts/name-service/src/test_helpers/assert.rs new file mode 100644 index 0000000000..b9c862906b --- /dev/null +++ b/contracts/name-service/src/test_helpers/assert.rs @@ -0,0 +1,65 @@ +use cosmwasm_std::{from_binary, testing::mock_env, Addr, Coin, Deps}; +use nym_name_service_common::{ + msg::QueryMsg, + response::{ConfigResponse, PagedNamesListResponse}, + NameEntry, NameId, +}; + +use crate::{constants::NAME_DEFAULT_RETRIEVAL_LIMIT, error::NameServiceError}; + +pub fn assert_config(deps: Deps, admin: &Addr, deposit_required: Coin) { + crate::state::assert_admin(deps, admin).unwrap(); + let res = crate::contract::query(deps, mock_env(), QueryMsg::Config {}).unwrap(); + let config: ConfigResponse = from_binary(&res).unwrap(); + assert_eq!(config, ConfigResponse { deposit_required }); +} + +pub fn assert_names(deps: Deps, expected_names: &[NameEntry]) { + let res = crate::contract::query(deps, mock_env(), QueryMsg::all()).unwrap(); + let names: PagedNamesListResponse = from_binary(&res).unwrap(); + let start_next_after = expected_names.iter().last().map(|s| s.name_id); + assert_eq!( + names, + PagedNamesListResponse { + names: expected_names.to_vec(), + per_page: NAME_DEFAULT_RETRIEVAL_LIMIT as usize, + start_next_after, + } + ); +} + +pub fn assert_name(deps: Deps, expected_name: &NameEntry) { + let res = crate::contract::query( + deps, + mock_env(), + QueryMsg::NameId { + name_id: expected_name.name_id, + }, + ) + .unwrap(); + let names: NameEntry = from_binary(&res).unwrap(); + assert_eq!(&names, expected_name); +} + +pub fn assert_empty(deps: Deps) { + let res = crate::contract::query(deps, mock_env(), QueryMsg::all()).unwrap(); + let names: PagedNamesListResponse = from_binary(&res).unwrap(); + assert!(names.names.is_empty()); +} + +pub fn assert_not_found(deps: Deps, expected_id: NameId) { + let res = crate::contract::query( + deps, + mock_env(), + QueryMsg::NameId { + name_id: expected_id, + }, + ) + .unwrap_err(); + assert!(matches!( + res, + NameServiceError::NotFound { + name_id: _expected_id + } + )); +} diff --git a/contracts/name-service/src/test_helpers/fixture.rs b/contracts/name-service/src/test_helpers/fixture.rs new file mode 100644 index 0000000000..3df82f6086 --- /dev/null +++ b/contracts/name-service/src/test_helpers/fixture.rs @@ -0,0 +1,29 @@ +use cosmwasm_std::Addr; +use nym_name_service_common::{Address, NameEntry, NameId, NymName, RegisteredName}; + +use super::helpers::nyms; + +pub fn name_fixture_full(name: &str, nym_address: &str, owner: &str) -> RegisteredName { + RegisteredName { + name: NymName::new(name).unwrap(), + address: Address::new(nym_address), + owner: Addr::unchecked(owner), + block_height: 12345, + deposit: nyms(100), + } +} + +pub fn name_fixture() -> RegisteredName { + name_fixture_full("my-service", "client_id.client_key@gateway_id", "steve") +} + +pub fn name_fixture_name(name: &str) -> RegisteredName { + name_fixture_full(name, "client_id.client_key@gateway_id", "steve") +} + +pub fn name_entry(name_id: NameId, name: NymName, address: Address, owner: Addr) -> NameEntry { + NameEntry { + name_id, + name: name_fixture_full(name.as_str(), address.as_str(), owner.as_str()), + } +} diff --git a/contracts/name-service/src/test_helpers/helpers.rs b/contracts/name-service/src/test_helpers/helpers.rs new file mode 100644 index 0000000000..0bd4f2ef5f --- /dev/null +++ b/contracts/name-service/src/test_helpers/helpers.rs @@ -0,0 +1,107 @@ +use cosmwasm_std::{ + coin, coins, + testing::{mock_dependencies, mock_env, mock_info, MockApi, MockQuerier}, + Coin, DepsMut, Event, MemoryStorage, OwnedDeps, Response, +}; +use cw_multi_test::AppResponse; +use nym_name_service_common::{ + events::{NameEventType, NAME_ID}, + msg::{ExecuteMsg, InstantiateMsg}, + NameId, NymName, RegisteredName, +}; + +pub fn nyms(amount: u64) -> Coin { + Coin::new(amount.into(), "unym") +} + +pub fn get_event_types(response: &Response, event_type: &str) -> Vec { + response + .events + .iter() + .filter(|ev| ev.ty == event_type) + .cloned() + .collect() +} + +pub fn get_attribute(response: &Response, event_type: &str, key: &str) -> String { + get_event_types(response, event_type) + .first() + .unwrap() + .attributes + .iter() + .find(|attr| attr.key == key) + .unwrap() + .value + .clone() +} + +pub fn get_app_event_types(response: &AppResponse, event_type: &str) -> Vec { + response + .events + .iter() + .filter(|ev| ev.ty == event_type) + .cloned() + .collect() +} + +pub fn get_app_attribute(response: &AppResponse, event_type: &str, key: &str) -> String { + get_app_event_types(response, event_type) + .first() + .unwrap() + .attributes + .iter() + .find(|attr| attr.key == key) + .unwrap() + .value + .clone() +} + +#[allow(unused)] +pub fn get_app_attributes(response: &AppResponse, event_type: &str, key: &str) -> Vec { + get_app_event_types(response, event_type) + .iter() + .map(|ev| { + ev.attributes + .iter() + .find(|attr| attr.key == key) + .unwrap() + .value + .clone() + }) + .collect::>() +} + +pub fn instantiate_test_contract() -> OwnedDeps { + let mut deps = mock_dependencies(); + let msg = InstantiateMsg { + deposit_required: coin(100, "unym"), + }; + let env = mock_env(); + let info = mock_info("creator", &[]); + let res = crate::instantiate(deps.as_mut(), env, info, msg).unwrap(); + assert_eq!(res.messages.len(), 0); + deps +} + +pub fn register_name(deps: DepsMut<'_>, name: &RegisteredName) -> NameId { + let msg: ExecuteMsg = name.clone().into(); + let info = mock_info(name.owner.as_str(), &coins(100, "unym")); + let res = crate::execute(deps, mock_env(), info, msg).unwrap(); + let name_id: NameId = get_attribute(&res, &NameEventType::Register.to_string(), NAME_ID) + .parse() + .unwrap(); + name_id +} + +pub fn delete_name_id(deps: DepsMut<'_>, name_id: NameId, owner: &str) { + let msg = ExecuteMsg::DeleteId { name_id }; + let info = mock_info(owner, &[]); + crate::execute(deps, mock_env(), info, msg).unwrap(); +} + +#[allow(unused)] +pub fn delete_name(deps: DepsMut<'_>, name: NymName, owner: &str) { + let msg = ExecuteMsg::DeleteName { name }; + let info = mock_info(owner, &[]); + crate::execute(deps, mock_env(), info, msg).unwrap(); +} diff --git a/contracts/name-service/src/test_helpers/mod.rs b/contracts/name-service/src/test_helpers/mod.rs new file mode 100644 index 0000000000..b1e4a18e01 --- /dev/null +++ b/contracts/name-service/src/test_helpers/mod.rs @@ -0,0 +1,4 @@ +pub mod assert; +pub mod fixture; +pub mod helpers; +pub mod test_setup; diff --git a/contracts/name-service/src/test_helpers/test_setup.rs b/contracts/name-service/src/test_helpers/test_setup.rs new file mode 100644 index 0000000000..98afbb60a0 --- /dev/null +++ b/contracts/name-service/src/test_helpers/test_setup.rs @@ -0,0 +1,153 @@ +use cosmwasm_std::{coins, Addr, Coin, Uint128}; +use cw_multi_test::{App, AppBuilder, AppResponse, ContractWrapper, Executor}; +use nym_name_service_common::{ + msg::{ExecuteMsg, InstantiateMsg, QueryMsg}, + response::{ConfigResponse, PagedNamesListResponse}, + Address, NameEntry, NameId, NymName, +}; +use serde::de::DeserializeOwned; + +use crate::test_helpers::helpers::get_app_attribute; + +const DENOM: &str = "unym"; +const ADDRESSES: &[&str] = &[ + "user", "admin", "owner", "owner1", "owner2", "owner3", "owner4", +]; +const WEALTHY_ADDRESSES: &[&str] = &["wealthy_owner_1", "wealthy_owner_2"]; + +/// Helper for being able to systematic integration tests +pub struct TestSetup { + app: App, + addr: Addr, +} + +impl Default for TestSetup { + fn default() -> Self { + TestSetup::new() + } +} + +impl TestSetup { + pub fn new() -> Self { + let mut app = AppBuilder::new().build(|router, _, storage| { + let mut init_balance = |account: &str, amount: u128| { + router + .bank + .init_balance(storage, &Addr::unchecked(account), coins(amount, DENOM)) + .unwrap(); + }; + ADDRESSES.iter().for_each(|addr| init_balance(addr, 250)); + WEALTHY_ADDRESSES + .iter() + .for_each(|addr| init_balance(addr, 1000)); + }); + let code = ContractWrapper::new(crate::execute, crate::instantiate, crate::query); + let code_id = app.store_code(Box::new(code)); + let addr = Self::instantiate(&mut app, code_id); + TestSetup { app, addr } + } + + fn instantiate(app: &mut App, code_id: u64) -> Addr { + app.instantiate_contract( + code_id, + Addr::unchecked("admin"), + &InstantiateMsg { + deposit_required: Coin::new(100, DENOM), + }, + &[], + "contract_label", + None, + ) + .unwrap() + } + + pub fn contract_balance(&self) -> Coin { + self.app.wrap().query_balance(&self.addr, DENOM).unwrap() + } + + pub fn query(&self, query_msg: &QueryMsg) -> T { + self.app + .wrap() + .query_wasm_smart(&self.addr, query_msg) + .unwrap() + } + + pub fn query_config(&self) -> ConfigResponse { + self.query(&QueryMsg::Config {}) + } + + pub fn query_id(&self, name_id: NameId) -> NameEntry { + self.query(&QueryMsg::NameId { name_id }) + } + + pub fn query_all(&self) -> PagedNamesListResponse { + self.query(&QueryMsg::all()) + } + + pub fn query_all_with_limit( + &self, + limit: Option, + start_after: Option, + ) -> PagedNamesListResponse { + self.query(&QueryMsg::All { limit, start_after }) + } + + pub fn try_register( + &mut self, + name: NymName, + address: Address, + owner: Addr, + ) -> anyhow::Result { + self.app.execute_contract( + owner, + self.addr.clone(), + &ExecuteMsg::Register { name, address }, + &[Coin { + denom: DENOM.to_string(), + amount: Uint128::new(100), + }], + ) + } + + pub fn register(&mut self, name: NymName, address: Address, owner: Addr) -> AppResponse { + let resp = self.try_register(name, address, owner).unwrap(); + assert_eq!( + get_app_attribute(&resp, "wasm-register", "action"), + "register" + ); + resp + } + + pub fn try_delete(&mut self, name_id: NameId, owner: Addr) -> anyhow::Result { + self.app.execute_contract( + owner, + self.addr.clone(), + &ExecuteMsg::DeleteId { name_id }, + &[], + ) + } + + pub fn delete(&mut self, name_id: NameId, owner: Addr) -> AppResponse { + let delete_resp = self.try_delete(name_id, owner).unwrap(); + assert_eq!( + get_app_attribute(&delete_resp, "wasm-delete_id", "action"), + "delete_id" + ); + delete_resp + } + + pub fn delete_name(&mut self, name: NymName, owner: Addr) -> AppResponse { + self.app + .execute_contract( + owner, + self.addr.clone(), + &ExecuteMsg::DeleteName { name }, + &[], + ) + .unwrap() + } + + pub fn balance(&self, address: impl Into) -> Coin { + self.app.wrap().query_balance(address, DENOM).unwrap() + } +} diff --git a/envs/qa-qwerty.env b/envs/qa-qwerty.env index 40cafc524b..ce4be6e28a 100644 --- a/envs/qa-qwerty.env +++ b/envs/qa-qwerty.env @@ -18,6 +18,7 @@ MULTISIG_CONTRACT_ADDRESS=n1gaq3666chd5348apj8cka8t2mckv7azp9espyr7wgpxyuzur5d0s COCONUT_DKG_CONTRACT_ADDRESS=n18yadscxw8v35dds7ksv3j0svmjh3h6e7tmxpadk96mvgz27zygkshuf4vs REWARDING_VALIDATOR_ADDRESS=n10yyd98e2tuwu0f7ypz9dy3hhjw7v772q6287gy SERVICE_PROVIDER_DIRECTORY_CONTRACT_ADDRESS=n1ryt076cufyddallg5x0gz3qjz0pd3wg0m4cwkg9njhmlnp6u88qq6nczgj +NAME_SERVICE_CONTRACT_ADDRESS=n1cm2u5vfjd3zalfw0p65xyh4tcrw3hjlm0960gzhewga449h4mgas77mjkl STATISTICS_SERVICE_DOMAIN_ADDRESS="https://mainnet-stats.nymte.ch:8090" NYXD="https://qwerty-validator.qa.nymte.ch/" NYM_API="https://qwerty-validator-api.qa.nymte.ch/api"