Store & Retrieve values with Maps

Prerequisites

Before getting started, make sure you have the following ready:

  1. You are generally familiar with command-line interfaces (CLI).

  2. You have installed Rust and set up your development environment as described in one of the sources below:

  3. We recommend going through both the flipper & the previous tutorials.

In the Develop the Counter smart contract section, you created a smart contract designed for the storage and retrieval of a singular numerical value. This tutorial demonstrates how to enhance your smart contract to handle a distinct number for each user by employing the Mapping type. The ink! language offers the Mapping type as a means to organize data in key-value pairs. For instance, the code below shows how to map a number to a specific user:

// Import the `Mapping` type
use ink::storage::Mapping;

#[ink(storage)]
pub struct MyContract {
  // Store a mapping from AccountIds to a u32
  my_map: Mapping<AccountId, u32>,
}

Using the Mapping data type allows you to maintain a distinct storage value for every key. In this tutorial, every AccountId serves as a unique key that is linked to a singular stored numeric value within my_map. Consequently, each user is limited to storing, increasing, and accessing the value tied to their specific AccountId.

Initialize a Mapping

The initial step involves setting up the mapping between an AccountId and its corresponding stored value. This requires specifying both the mapping key and the value it is associated with. The example below demonstrates how to establish a Mapping and accessing a value:

#![cfg_attr(not(feature = "std"), no_std)]

#[ink::contract]
mod mycontract {
    use ink::storage::Mapping;

    #[ink(storage)]
    pub struct MyContract {
        // Store a mapping from AccountIds to a u32
        my_map: Mapping<AccountId, u32>,
    }

    impl MyContract {
        #[ink(constructor)]
        pub fn new(count: u32) -> Self {
            let mut my_map = Mapping::default();
            let caller = Self::env().caller();
            my_map.insert(&caller, &count);

            Self { my_map }
        }

        // Get the number associated with the caller's AccountId, if it exists
        #[ink(message)]
        pub fn get(&self) -> u32 {
            let caller = Self::env().caller();
            self.my_map.get(&caller).unwrap_or_default()
        }
    }
}

Setting the Caller of the Contract

In the example provided earlier, you may have observed the usage of the Self::env().caller() function. This function, which can be invoked at any point within the contract's logic, consistently identifies the entity calling the contract. It's crucial to distinguish between the contract caller and the origin caller. If a contract invoked by a user subsequently triggers another contract, the Self::env().caller() in this secondary contract refers to the first contract's address, not the initiating user's.

Applying the Contract Caller Information

Understanding the identity of the contract caller is beneficial in various contexts. For instance, Self::env().caller() can be leveraged to establish an access control mechanism that restricts users from interacting only with their data. Additionally, it can be utilized to record the contract's owner at the time of its deployment, as shown in the following example:

#![cfg_attr(not(feature = "std"), no_std)]

#[ink::contract]
mod my_contract {

    #[ink(storage)]
    pub struct MyContract {
        // Store a contract owner
        owner: AccountId,
    }

    impl MyContract {
        #[ink(constructor)]
        pub fn new() -> Self {
            Self {
                owner: Self::env().caller(),
            }
        }
        /* --snip-- */
    }
}

By storing the contract caller under the owner identifier, you can subsequently develop functions to verify if the present contract caller is the contract's owner.

Add mapping to the smart contract

You are now ready to introduce a storage map to the incrementer contract.

To add a storage map to the incrementer contract:

  1. Open a terminal shell on your computer, if needed.

  2. Verify that you are in the incrementer project folder.

  3. Open the lib.rs file in a text editor.

  4. Import the Mapping type.

    #[ink::contract
    mod incrementer {
       use ink::storage::Mapping;
  5. Add the mapping key from AccountId to the i32 data type stored as my_map.

    pub struct Incrementer {
       value: i32,
       my_map: Mapping<AccountId, i32>,
    }
  6. In the new constructor create a new Mapping and use that to initialize your contract.

    #[ink(constructor)]
    pub fn new(init_value: i32) -> Self {
       let mut my_map = Mapping::default();
       let caller = Self::env().caller();
       my_map.insert(&caller, &0);
    
       Self {
           value: init_value,
           my_map,
       }
    }
  7. In the default constructor add a new default Mapping along with the already defined default value.

    #[ink(constructor)]
    pub fn default() -> Self {
    Self {
            value: 0,
            my_map: Mapping::default(),
        }
    }
  8. Add a get_mine() function to read my_map using the Mapping API's get() method and return my_map for the contract caller.

    #[ink(message)]
    pub fn get_mine(&self) -> i32 {
        let caller = self.env().caller();
        self.my_map.get(&caller).unwrap_or_default()
    }
  9. Add a new test to the initialize accounts.

    #[ink::test]
    fn my_map_works() {
       let contract = Incrementer::new(11);
       assert_eq!(contract.get(), 11);
       assert_eq!(contract.get_mine(), 0);
    }
  10. Save your changes and close the file.

  11. Use the test subcommand and nightly toolchain to test your work by running the following command:

    cargo test

    The command should display output similar to the following to indicate successful test completion:

    running 3 tests
    test incrementer::tests::default_works ... ok
    test incrementer::tests::it_works ... ok
    test incrementer::tests::my_map_works ... ok
    
    test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Enabling Values Modification

The final step of the Incrementer contract involves empowering users to modify their specific values. This capability can be facilitated through the use of the Mapping API within the smart contract, which grants straightforward access to storage items. For instance, to replace an existing value for a storage item, you can utilize the Mapping::insert() function with a previously used key. Moreover, values can be updated by initially retrieving them from storage with Mapping::get(), followed by applying Mapping::insert() to input the new value. Should Mapping::get() find no current value for a specified key, it will return None. Given that the Mapping API offers unmediated access to storage, the Mapping::remove() function can be employed to eliminate the value associated with a particular key from storage. To add functions for inserting and removing values into the contract:

  1. Open a terminal shell on your computer, if needed.

  2. Verify that you are in the incrementer project folder.

  3. Open the lib.rs file in a text editor.

  4. Add an inc_mine() function that allows the contract caller to get the my_map storage item and insert an incremented value into the mapping.

    #[ink(message)]
    pub fn inc_mine(&mut self, by: i32) {
       let caller = self.env().caller();
       let my_value = self.get_mine();
       self.my_map.insert(caller, &(my_value + by));
    }
  5. Add a remove_mine() function that allows the contract caller to clear the my_map storage item from storage.

    #[ink(message)]
    pub fn remove_mine(&self) {
       let caller = self.env().caller();
       self.my_map.remove(&caller)
    }
  6. Add a new test to verify that the inc_mine() functions works as expected.

    #[ink::test]
    fn inc_mine_works() {
       let mut contract = Incrementer::new(11);
       assert_eq!(contract.get_mine(), 0);
       contract.inc_mine(5);
       assert_eq!(contract.get_mine(), 5);
       contract.inc_mine(5);
       assert_eq!(contract.get_mine(), 10);
    }
  7. Add a new test to verify that the remove_mine() functions works as expected.

    #[ink::test]
    fn remove_mine_works() {
       let mut contract = Incrementer::new(11);
       assert_eq!(contract.get_mine(), 0);
       contract.inc_mine(5);
       assert_eq!(contract.get_mine(), 5);
       contract.remove_mine();
       assert_eq!(contract.get_mine(), 0);
    }
  8. Check your work using the test subcommand:

    cargo test

    The command should display output similar to the following to indicate successful test completion:

    running 5 tests
    test incrementer::tests::default_works ... ok
    test incrementer::tests::it_works ... ok
    test incrementer::tests::remove_mine_works ... ok
    test incrementer::tests::inc_mine_works ... ok
    test incrementer::tests::my_map_works ... ok
    
    test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Last updated