使用映射存储值

⚠️ Update Notice:

Please read Substrate to Polkadot SDK page first.


开发智能合约中,您开发了一个用于存储和检索单个数值的智能合约。

本教程说明了如何扩展智能合约的功能以管理每个用户的单个数字。 要添加此功能,您将使用Mapping类型。

ink!语言提供 Mapping 类型,使您可以将数据存储为键值对。例如,以下代码说明了如何将用户映射到数字:

// 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>,
}

使用Mapping数据类型,您可以为每个键存储存储值的唯一实例。

在本教程中,每个AccountId都表示一个键,该键映射到一个且只有一个存储的数值my_map

每个用户只能存储、递增和检索与其自身AccountId关联的值。

初始化Mapping

第一步是初始化AccountId和存储值之间的映射。

  • 指定映射键及其映射到的值。

以下示例说明了如何初始化Mapping并检索值:

#![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()
        }
    }
}

确定合约调用者

在前面的示例中,您可能已经注意到了Self::env().caller()函数调用。

此函数在整个合约逻辑中都可用,并且始终返回合约调用者

重要的是要注意,合约调用者与来源调用者不同。

如果用户访问一个合约,然后调用后续合约,则第二个合约中的Self::env().caller()是第一个合约的地址,而不是原始用户。

使用合约调用者

在许多情况下,拥有可用的合约调用者都很有用。

例如,您可以使用Self::env().caller()创建一个访问控制层,该层仅允许用户访问他们自己的值。

您还可以使用Self::env().caller()在合约部署期间保存合约所有者。

例如:

#![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-- */
    }
}

因为您已使用owner标识符保存了合约调用者,所以您可以稍后编写函数来检查当前合约调用者是否是合约的所有者。

向智能合约添加映射

您现在可以向incrementer合约引入存储映射了。

要向incrementer合约添加存储映射:

  1. 根据需要在您的计算机上打开终端 shell。
  2. 验证您是否位于incrementer项目文件夹中。
  3. 在文本编辑器中打开lib.rs文件。
  4. 导入Mapping类型。

    #[ink::contract
    mod incrementer {
       use ink::storage::Mapping;
  5. 将映射键从AccountId添加到存储为my_mapi32数据类型。

    pub struct Incrementer {
       value: i32,
       my_map: Mapping<AccountId, i32>,
    }
  6. new构造函数中创建一个新的Mapping,并使用它来初始化您的合约。

    #[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. default构造函数中添加新的默认Mapping以及已定义的默认value

    #[ink(constructor)]
    pub fn default() -> Self {
    Self {
            value: 0,
            my_map: Mapping::default(),
        }
    }
  8. 添加一个get_mine()函数,以使用 Mapping API 的get()方法读取my_map并返回合约调用者的my_map

    #[ink(message)]
    pub fn get_mine(&self) -> i32 {
        let caller = self.env().caller();
        self.my_map.get(&caller).unwrap_or_default()
    }
  9. 添加新的测试以初始化帐户。

    #[ink::test]
    fn my_map_works() {
       let contract = Incrementer::new(11);
       assert_eq!(contract.get(), 11);
       assert_eq!(contract.get_mine(), 0);
    }
  10. 保存更改并关闭文件。
  11. 使用test子命令和nightly工具链通过运行以下命令来测试您的工作:

    cargo test

    该命令应显示类似于以下内容的输出,以指示测试已成功完成:

    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

插入、更新或删除值

Incrementer合约的最后一步是允许用户更新他们自己的值。

您可以使用对 Mapping API 的调用在智能合约中提供此功能。

Mapping提供对存储项的直接访问。

例如,您可以通过使用现有键调用Mapping::insert()来替换存储项先前保存的值。

您还可以通过首先使用Mapping::get()从存储中读取它们,然后使用Mapping::insert()更新值来更新值。

如果给定键处没有现有值,则Mapping::get()返回None

由于 Mapping API 提供对存储的直接访问,因此您可以使用Mapping::remove()方法从存储中删除给定键处的值。

要向合约添加插入和删除函数:

  1. 根据需要在您的计算机上打开终端 shell。
  2. 验证您是否位于incrementer项目文件夹中。
  3. 在文本编辑器中打开lib.rs文件。
  4. 添加一个inc_mine()函数,允许合约调用者获取my_map存储项并将递增的value插入到映射中。

    #[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. 添加一个remove_mine()函数,允许合约调用者从存储中清除my_map存储项。

    #[ink(message)]
    pub fn remove_mine(&self) {
       let caller = self.env().caller();
       self.my_map.remove(&caller)
    }
  6. 添加新的测试以验证inc_mine()函数是否按预期工作。

    #[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. 添加新的测试以验证remove_mine()函数是否按预期工作。

    #[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. 使用test子命令检查您的工作:

    cargo test

    该命令应显示类似于以下内容的输出,以指示测试已成功完成:

    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

下一步

在本教程中,您学习了如何在智能合约中使用ink::storage::Mapping类型和 Mapping API。例如,本教程说明了:

  • 如何初始化用于存储键值对的映射。
  • 如何在智能合约中识别和使用合约调用者。
  • 如何添加函数,使用户能够使用智能合约在映射中插入和删除为他们存储的值。

您可以在智能合约的资产中找到本教程最终代码的示例。

您可以在以下主题中了解有关智能合约开发的更多信息: