From efdc5a390e0f60b3ef7281bbb5ea0cec60751ed0 Mon Sep 17 00:00:00 2001 From: Hero Bird Date: Fri, 13 Mar 2020 15:35:33 +0100 Subject: [PATCH] Add DomainNameService example contract (#293) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [examples] add DomainNameService example contract * [examples] remove unnecessary constructor param * [examples] fix clippy warning * [examples] apply rustfmt * [examples] update SCALE 1.1 -> 1.2 * [example] generally update the DNS contract example * [examples] add Debug, PartialEq and Eq derives to Error * [examples] add tests to DomainNameService example contract * [lang] move default off-chain init before contract init This is to prevent failures where the constructor of the contract already accesses certain off-chain fields. * [examples] remove .cargo/config - no longer needed * [examples] fix abi_gen impl for DNS contract * fix typo Co-Authored-By: Michael Müller Co-authored-by: Michael Müller --- examples/dns/.gitignore | 9 + examples/dns/.ink/abi_gen/Cargo.toml | 16 ++ examples/dns/.ink/abi_gen/main.rs | 7 + examples/dns/Cargo.toml | 60 ++++++ examples/dns/lib.rs | 262 +++++++++++++++++++++++++++ lang/macro/src/codegen/testable.rs | 4 +- 6 files changed, 356 insertions(+), 2 deletions(-) create mode 100644 examples/dns/.gitignore create mode 100644 examples/dns/.ink/abi_gen/Cargo.toml create mode 100644 examples/dns/.ink/abi_gen/main.rs create mode 100644 examples/dns/Cargo.toml create mode 100644 examples/dns/lib.rs diff --git a/examples/dns/.gitignore b/examples/dns/.gitignore new file mode 100644 index 00000000..bf910de1 --- /dev/null +++ b/examples/dns/.gitignore @@ -0,0 +1,9 @@ +# Ignore build artifacts from the local tests sub-crate. +/target/ + +# Ignore backup files creates by cargo fmt. +**/*.rs.bk + +# Remove Cargo.lock when creating an executable, leave it for libraries +# More information here http://doc.crates.io/guide.html#cargotoml-vs-cargolock +Cargo.lock \ No newline at end of file diff --git a/examples/dns/.ink/abi_gen/Cargo.toml b/examples/dns/.ink/abi_gen/Cargo.toml new file mode 100644 index 00000000..fde0e2cc --- /dev/null +++ b/examples/dns/.ink/abi_gen/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "abi-gen" +version = "0.1.0" +authors = ["Parity Technologies "] +edition = "2018" +publish = false + +[[bin]] +name = "abi-gen" +path = "main.rs" + +[dependencies] +contract = { path = "../..", package = "dns", default-features = false, features = ["ink-generate-abi"] } +ink_lang = { path = "../../../../lang", default-features = false, features = ["ink-generate-abi"] } +serde = "1.0" +serde_json = "1.0" diff --git a/examples/dns/.ink/abi_gen/main.rs b/examples/dns/.ink/abi_gen/main.rs new file mode 100644 index 00000000..3132baae --- /dev/null +++ b/examples/dns/.ink/abi_gen/main.rs @@ -0,0 +1,7 @@ +fn main() -> Result<(), std::io::Error> { + let abi = ::generate_abi(); + let contents = serde_json::to_string_pretty(&abi)?; + std::fs::create_dir("target").ok(); + std::fs::write("target/metadata.json", contents)?; + Ok(()) +} diff --git a/examples/dns/Cargo.toml b/examples/dns/Cargo.toml new file mode 100644 index 00000000..d0812977 --- /dev/null +++ b/examples/dns/Cargo.toml @@ -0,0 +1,60 @@ +[package] +name = "dns" +version = "0.1.0" +authors = ["Parity Technologies "] +edition = "2018" + +[dependencies] +ink_primitives = { path = "../../primitives", default-features = false } +ink_abi = { path = "../../abi", default-features = false, features = ["derive"], optional = true } +ink_core = { path = "../../core", default-features = false } +ink_lang = { path = "../../lang", default-features = false } + +scale = { package = "parity-scale-codec", version = "1.2", default-features = false, features = ["derive"] } +type-metadata = { git = "https://github.com/type-metadata/type-metadata.git", default-features = false, features = ["derive"], optional = true } + +[lib] +name = "dns" +path = "lib.rs" +crate-type = [ + # Used for normal contract Wasm blobs. + "cdylib", + # Used for ABI generation. + "rlib", +] + +[features] +default = ["test-env"] +std = [ + "ink_primitives/std", + "ink_abi/std", + "ink_core/std", + "scale/std", + "type-metadata/std", +] +test-env = [ + "std", + "ink_lang/test-env", +] +ink-generate-abi = [ + "std", + "ink_abi", + "type-metadata", + "ink_core/ink-generate-abi", + "ink_lang/ink-generate-abi", +] +ink-as-dependency = [] + +[profile.release] +panic = "abort" +lto = true +opt-level = "z" +overflow-checks = true + +[workspace] +members = [ + ".ink/abi_gen" +] +exclude = [ + ".ink" +] diff --git a/examples/dns/lib.rs b/examples/dns/lib.rs new file mode 100644 index 00000000..13d3f7a0 --- /dev/null +++ b/examples/dns/lib.rs @@ -0,0 +1,262 @@ +// Copyright 2019-2020 Parity Technologies (UK) Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#![cfg_attr(not(feature = "std"), no_std)] + +use ink_lang as ink; + +#[ink::contract(version = "0.1.0")] +mod dns { + use ink_core::storage; + use scale; + + /// Emitted whenever a new name is being registered. + #[ink(event)] + struct Register { + #[ink(topic)] + name: Hash, + #[ink(topic)] + from: AccountId, + } + + /// Emitted whenever an address changes. + #[ink(event)] + struct SetAddress { + #[ink(topic)] + name: Hash, + from: AccountId, + #[ink(topic)] + old_address: Option, + #[ink(topic)] + new_address: AccountId, + } + + /// Emitted whenver a name is being transferred. + #[ink(event)] + struct Transfer { + #[ink(topic)] + name: Hash, + from: AccountId, + #[ink(topic)] + old_owner: Option, + #[ink(topic)] + new_owner: AccountId, + } + + /// Domain name service contract inspired by ChainX's [blog post] + /// (https://medium.com/@chainx_org/secure-and-decentralized-polkadot-domain-name-system-e06c35c2a48d). + /// + /// # Note + /// + /// This is a port from the blog post's ink! 1.0 based version of the contract + /// to ink! 2.0. + /// + /// # Description + /// + /// The main function of this contract is domain name resolution which + /// refers to the retrieval of numeric values corresponding to readable + /// and easily memorable names such as “polka.dot” which can be used + /// to facilitate transfers, voting and dapp-related operations instead + /// of resorting to long IP addresses that are hard to remember. + #[ink(storage)] + struct DomainNameService { + /// A hashmap to store all name to addresses mapping. + name_to_address: storage::HashMap, + /// A hashmap to store all name to owners mapping. + name_to_owner: storage::HashMap, + /// The default address. + default_address: storage::Value, + } + + /// Errors that can occur upon calling this contract. + #[derive(Debug, PartialEq, Eq, scale::Encode, scale::Decode)] + pub enum Error { + /// Returned if the name already exists upon registration. + NameAlreadyExists, + /// Returned if caller is not owner while required to. + CallerIsNotOwner, + } + + /// Type alias for the contract's result type. + pub type Result = core::result::Result; + + impl DomainNameService { + /// Creates a new domain name service contract. + #[ink(constructor)] + fn new(&mut self) { + self.default_address.set(AccountId::from([0x00; 32])); + } + + /// Register specific name with caller as owner. + #[ink(message)] + fn register(&mut self, name: Hash) -> Result<()> { + let caller = self.env().caller(); + if self.is_name_assigned(name) { + return Err(Error::NameAlreadyExists) + } + self.name_to_owner.insert(name, caller); + self.env().emit_event(Register { name, from: caller }); + Ok(()) + } + + /// Set address for specific name. + #[ink(message)] + fn set_address(&mut self, name: Hash, new_address: AccountId) -> Result<()> { + let caller = self.env().caller(); + let owner = self.get_owner_or_default(name); + if caller != owner { + return Err(Error::CallerIsNotOwner) + } + let old_address = self.name_to_address.insert(name, new_address); + self.env().emit_event(SetAddress { + name, + from: caller, + old_address, + new_address, + }); + Ok(()) + } + + /// Transfer owner to another address. + #[ink(message)] + fn transfer(&mut self, name: Hash, to: AccountId) -> Result<()> { + let caller = self.env().caller(); + let owner = self.get_owner_or_default(name); + if caller != owner { + return Err(Error::CallerIsNotOwner) + } + let old_owner = self.name_to_owner.insert(name, to); + self.env().emit_event(Transfer { + name, + from: caller, + old_owner, + new_owner: to, + }); + Ok(()) + } + + /// Get address for specific name. + #[ink(message)] + fn get_address(&self, name: Hash) -> AccountId { + self.get_address_or_default(name) + } + + /// Returns `true` if the name already assigned. + #[ink(message)] + fn is_name_assigned(&self, name: Hash) -> bool { + self.name_to_owner.get(&name).is_some() + } + + /// Returns the owner given the hash or the default address. + fn get_owner_or_default(&self, name: Hash) -> AccountId { + *self + .name_to_owner + .get(&name) + .unwrap_or(&*self.default_address) + } + + /// Returns the address given the hash or the default address. + fn get_address_or_default(&self, name: Hash) -> AccountId { + *self + .name_to_address + .get(&name) + .unwrap_or(&*self.default_address) + } + } + + #[cfg(test)] + mod tests { + use super::*; + use ink_core::env; + + const DEFAULT_CALLEE_HASH: [u8; 32] = [0x07; 32]; + const DEFAULT_ENDOWMENT: Balance = 1_000_000; + const DEFAULT_GAS_LIMIT: Balance = 1_000_000; + + fn default_accounts() -> env::test::DefaultAccounts { + env::test::default_accounts::() + .expect("off-chain environment should have been initialized already") + } + + fn set_next_caller(caller: AccountId) { + env::test::push_execution_context::( + caller, + AccountId::from(DEFAULT_CALLEE_HASH), + DEFAULT_ENDOWMENT, + DEFAULT_GAS_LIMIT, + env::call::CallData::new(env::call::Selector::from_str("")), + ) + } + + #[test] + fn register_works() { + let default_accounts = default_accounts(); + let name = Hash::from([0x99; 32]); + + set_next_caller(default_accounts.alice); + let mut contract = DomainNameService::new(); + + assert_eq!(contract.register(name), Ok(())); + assert_eq!(contract.register(name), Err(Error::NameAlreadyExists)); + } + + #[test] + fn set_address_works() { + let accounts = default_accounts(); + let name = Hash::from([0x99; 32]); + + set_next_caller(accounts.alice); + + let mut contract = DomainNameService::new(); + assert_eq!(contract.register(name), Ok(())); + + // Caller is not owner, `set_address` should fail. + set_next_caller(accounts.bob); + assert_eq!( + contract.set_address(name, accounts.bob), + Err(Error::CallerIsNotOwner) + ); + + // caller is owner, set_address will be successful + set_next_caller(accounts.alice); + assert_eq!(contract.set_address(name, accounts.bob), Ok(())); + assert_eq!(contract.get_address(name), accounts.bob); + } + + #[test] + fn transfer_works() { + let accounts = default_accounts(); + let name = Hash::from([0x99; 32]); + + set_next_caller(accounts.alice); + + let mut contract = DomainNameService::new(); + assert_eq!(contract.register(name), Ok(())); + + // Test transfer of owner. + assert_eq!(contract.transfer(name, accounts.bob), Ok(())); + + // Owner is bob, alice `set_address` should fail. + assert_eq!( + contract.set_address(name, accounts.bob), + Err(Error::CallerIsNotOwner) + ); + + set_next_caller(accounts.bob); + // Now owner is bob, `set_address` should be successful. + assert_eq!(contract.set_address(name, accounts.bob), Ok(())); + assert_eq!(contract.get_address(name), accounts.bob); + } + } +} diff --git a/lang/macro/src/codegen/testable.rs b/lang/macro/src/codegen/testable.rs index 0e84286f..fca7691c 100644 --- a/lang/macro/src/codegen/testable.rs +++ b/lang/macro/src/codegen/testable.rs @@ -56,6 +56,8 @@ impl GenerateCode for TestWrapper<'_> { type Wrapped = TestableStorage; fn instantiate() -> Self::Wrapped { + ink_core::env::test::initialize_as_default::() + .expect("encountered already initialized off-chain environment"); let mut contract: Self = unsafe { let mut alloc = ink_core::storage::alloc::BumpAlloc::from_raw_parts( @@ -65,8 +67,6 @@ impl GenerateCode for TestWrapper<'_> { &mut alloc, ) }; - ink_core::env::test::initialize_as_default::() - .expect("encountered already initialized off-chain environment"); ink_core::storage::alloc::Initialize::try_default_initialize( &mut contract, ); -- GitLab