开发智能合约

⚠️ Update Notice:

Please read Substrate to Polkadot SDK page first.


准备您的第一个合约中,您学习了使用默认的第一个项目在基于 Substrate 的区块链上构建和部署智能合约的基本步骤。

在本教程中,您将开发一个新的智能合约,每次执行函数调用时都会递增计数器值。

开始之前

在开始之前,请验证以下内容:

  • 您拥有良好的互联网连接,并且可以访问本地计算机上的 shell 终端。
  • 您通常熟悉软件开发和使用命令行界面。
  • 您通常熟悉区块链和智能合约平台。
  • 您已按照安装中的说明安装 Rust 并设置了开发环境。
  • 您已完成准备您的第一个合约,并在本地安装了 Substrate 合约节点。

教程目标

通过完成本教程,您将实现以下目标:

  • 学习如何使用智能合约模板。
  • 使用智能合约存储简单值。
  • 使用智能合约递增和检索存储的值。
  • 向智能合约添加公共函数和私有函数。

智能合约和 ink!

准备您的第一个合约中,您安装了cargo-contract包,以便通过命令行访问 ink!编程语言。

ink!语言是嵌入式领域特定语言

此语言使您可以使用 Rust 编程语言编写基于 WebAssembly 的智能合约。

该语言使用带有专用#[ink(...)]属性宏的标准 Rust 模式。

这些属性宏描述了智能合约的不同部分所代表的内容,以便可以将其转换为与 Substrate 兼容的 WebAssembly 字节码。

创建新的智能合约项目

在 Substrate 上运行的智能合约以项目的形式开始。 您可以使用cargo contract命令创建项目。

在本教程中,您将为incrementer智能合约创建一个新项目。

创建新项目会向项目目录添加新的项目目录和默认的入门文件(也称为模板文件)。

您将修改这些入门模板文件以构建incrementer项目的智能合约逻辑。

要为智能合约创建新项目:

  1. 如果您还没有打开终端 shell,请在本地计算机上打开一个。
  2. 通过运行以下命令创建一个名为incrementer的新项目:

    cargo contract new incrementer
  3. 通过运行以下命令更改到新项目目录:

    cd incrementer/
  4. 在文本编辑器中打开lib.rs文件。

    默认情况下,模板lib.rs文件包含flipper智能合约的源代码,其中flipper合约名称的实例已重命名为incrementer

  5. 将默认模板源代码替换为新的incrementer源代码。
  6. 保存对lib.rs文件的更改,然后关闭该文件。
  7. 通过运行以下命令验证程序是否已编译并通过了简单的测试:

    cargo test

    您可以忽略任何警告,因为此模板代码只是一个框架。 该命令应显示类似于以下内容的输出,以指示测试已成功完成:

    running 1 test
    test incrementer::tests::default_works ... ok
    
    test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
  8. 通过运行以下命令验证您是否可以构建合约的 WebAssembly:

    cargo contract build

    如果程序成功编译,则您可以开始编程。

存储简单值

现在您已经拥有了incrementer智能合约的一些入门源代码,您可以引入一些新功能。

例如,此智能合约需要存储简单值。

本节中的以下代码旨在说明 ink!语言的功能。 您将在本教程的下一节更新您的智能合约中开始使用代码。

您可以使用#[ink(storage)]属性宏为合约存储简单值:

#[ink(storage)]
pub struct MyContract {
  // Store a bool
  my_bool: bool,
  // Store a number
  my_number: u32,
}

支持的类型

ink!智能合约支持大多数 Rust 常用数据类型,包括布尔值、无符号整数和有符号整数、字符串、元组和数组。

这些数据类型使用Parity scale 编解码器进行编码和解码,以便高效地通过网络传输。

除了可以使用 scale 编解码器进行编码和解码的常用 Rust 类型外,ink!语言还支持 Substrate 特定的类型(如AccountIdBalanceHash),就好像它们是基本类型一样。

以下代码说明了如何为此合约存储AccountIdBalance

#[ink::contract]
mod MyContract {

  // Our struct will use those default ink! types
  #[ink(storage)]
  pub struct MyContract {
    // Store some AccountId
    my_account: AccountId,
    // Store some Balance
    my_balance: Balance,
  }
/* --snip-- */
}

构造函数

每个 ink!智能合约都必须至少有一个在创建合约时运行的构造函数。 但是,智能合约可以根据需要有多个构造函数。

以下代码说明了如何使用多个构造函数:

#[ink::contract]
mod my_contract {

    #[ink(storage)]
    pub struct MyContract {
        number: u32,
    }

    impl MyContract {
        /// Constructor that initializes the `u32` value to the given `init_value`.
        #[ink(constructor)]
        pub fn new(init_value: u32) -> Self {
            Self {
                number: init_value,
            }
        }

        /// Constructor that initializes the `u32` value to the `u32` default (0).
        ///
        /// Constructors can delegate to other constructors.
        #[ink(constructor)]
        pub fn default() -> Self {
            Self {
                number: Default::default(),
            }
        }
    /* --snip-- */
    }
}

更新您的智能合约

现在您已经学习了存储简单值、声明数据类型和使用构造函数,您可以更新智能合约源代码以实现以下内容:

  • 创建一个名为value的存储值,其数据类型为i32
  • 创建一个新的Incrementer构造函数,并将其value设置为init_value
  • 创建一个名为default的第二个构造函数,该函数没有输入,并创建一个新的Incrementer,其value设置为0

要更新智能合约:

  1. 在文本编辑器中打开lib.rs文件。
  2. 通过声明名为value且数据类型为i32的存储项来替换“存储声明”注释。

    #[ink(storage)]
    pub struct Incrementer {
       value: i32,
    }
  3. 修改Incrementer构造函数以将其value设置为init_value

    impl Incrementer {
        #[ink(constructor)]
        pub fn new(init_value: i32) -> Self {
            Self { value: init_value }
        }
    }
  4. 添加一个名为default的第二个构造函数,该函数创建一个新的Incrementer,其value设置为0

    #[ink(constructor)]
    pub fn default() -> Self {
       Self {
           value: 0,
       }
    }
  5. 保存更改并关闭文件。
  6. 尝试再次运行test子命令,您会看到测试现在失败了。这是因为我们需要更新get函数并修改测试以匹配我们实现的更改。我们将在下一节中进行此操作。

添加一个函数来获取存储值

现在您已经创建并初始化了存储值,您可以使用公共函数和私有函数与之交互。 在本教程中,您将添加一个公共函数(也称为消息)来获取存储值。

请注意,所有公共函数都必须使用#[ink(message)]属性宏。

要向智能合约添加公共函数:

  1. 在文本编辑器中打开lib.rs文件。
  2. 更新get公共函数以返回具有i32数据类型的value存储项的数据。

    #[ink(message)]
    pub fn get(&self) -> i32 {
       self.value
    }

    由于此函数仅读取合约存储,因此它使用&self参数来访问合约函数和存储项。

    此函数不允许更改value存储项的状态。

    如果函数中的最后一个表达式没有分号(;),Rust 会将其视为返回值。

  3. 将私有default_works函数中的“测试您的合约”注释替换为测试get函数的代码。

    #[ink::test]
    fn default_works() {
       let contract = Incrementer::default();
       assert_eq!(contract.get(), 0);
    }
  4. 保存更改并关闭文件。
  5. 使用test子命令检查您的工作,您会看到它仍然失败,因为我们需要更新it_works测试并添加一个新的公共函数来递增value存储项。

    cargo test

添加一个函数来修改存储值

此时,智能合约不允许用户修改存储。 要启用用户修改存储项,您必须将value显式标记为可变变量。

要添加用于递增存储值的函数:

  1. 在文本编辑器中打开lib.rs文件。
  2. 添加一个新的inc公共函数,使用数据类型为i32by参数来递增存储的value

    #[ink(message)]
    pub fn inc(&mut self, by: i32) {
       self.value += by;
    }
  3. 向源代码添加新的测试以验证此函数。

    #[ink::test]
    fn it_works() {
       let mut contract = Incrementer::new(42);
       assert_eq!(contract.get(), 42);
       contract.inc(5);
       assert_eq!(contract.get(), 47);
       contract.inc(-50);
       assert_eq!(contract.get(), -3);
    }
  4. 保存更改并关闭文件。
  5. 使用test子命令检查您的工作:

    cargo test

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

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

构建合约的 WebAssembly

测试incrementer合约后,您可以将此项目编译为 WebAssembly。

要为此智能合约构建 WebAssembly:

  1. 根据需要在您的计算机上打开终端 shell。
  2. 验证您是否位于incrementer项目文件夹中。
  3. 通过运行以下命令编译incrementer智能合约:

    cargo contract build

    该命令显示类似于以下内容的输出:

    Your contract artifacts are ready. You can find them in:
    /Users/dev-docs/incrementer/target/ink
    
    - incrementer.contract (code + metadata)
    - incrementer.wasm (the contract's code)
    - incrementer.json (the contract's metadata)

部署和测试智能合约

如果您在本地安装了substrate-contracts-node节点,则可以为智能合约启动本地区块链节点。

然后,您可以使用cargo-contract来部署和测试智能合约。

要在本地节点上部署:

  1. 根据需要在您的计算机上打开终端 shell。
  2. 通过运行以下命令以本地开发模式启动合约节点:

    substrate-contracts-node --log info,runtime::contracts=debug 2>&1
  3. 上传并实例化合约

    cargo contract instantiate --constructor default --suri //Alice --salt $(date +%s)
    Dry-running default (skip with --skip-dry-run)
       Success! Gas required estimated at Weight(ref_time: 321759143, proof_size: 0)
    Confirm transaction details: (skip with --skip-confirm)
    Constructor default
           Args
      Gas limit Weight(ref_time: 321759143, proof_size: 0)
    Submit? (Y/n):
      Events
       Event Balances ➜ Withdraw
         who: 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY
         amount: 2.953956313mUNIT
       ... snip ...
       Event System ➜ ExtrinsicSuccess
         dispatch_info: DispatchInfo { weight: Weight { ref_time: 2772097885, proof_size: 0 }, class: Normal, pays_fee: Yes }
    
    Code hash 0x71ddef2422fdb8358b503d5ef122c088a2dc6486dd460c37b01d672a8d319959
    Contract 5Cf6wFEyZnqvNJaKVxnWswefo7uT4jVsgzWKh8b78GLDV6kN
  4. 递增值

    cargo contract call --contract $INSTANTIATED_CONTRACT_ADDRESS --message inc --args 42 --suri //Alice
 Dry-running inc (skip with --skip-dry-run)
  Success! Gas required estimated at Weight(ref_time: 8013742080, proof_size: 262144)
 Confirm transaction details: (skip with --skip-confirm)
      Message inc
         Args 42
    Gas limit Weight(ref_time: 8013742080, proof_size: 262144)
 Submit? (Y/n):
    Events
     Event Balances ➜ Withdraw
       who: 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY
       amount: 98.97416μUNIT
     Event Contracts ➜ Called
       caller: 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY
       contract: 5Cf6wFEyZnqvNJaKVxnWswefo7uT4jVsgzWKh8b78GLDV6kN
     Event TransactionPayment ➜ TransactionFeePaid
       who: 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY
       actual_fee: 98.97416μUNIT
       tip: 0UNIT
     Event System ➜ ExtrinsicSuccess
       dispatch_info: DispatchInfo { weight: Weight { ref_time: 1383927346, proof_size: 13255 }, class: Normal, pays_fee: Yes }
  1. 获取当前值

    cargo contract call --contract 5Cf6wFEyZnqvNJaKVxnWswefo7uT4jVsgzWKh8b78GLDV6kN --message get --suri //Alice --dry-run
    Result Success!
    Reverted false
     Data Tuple(Tuple { ident: Some("Ok"), values: [Int(42)] })

正如您所看到的,从合约中读取的value42,这与我们之前的步骤匹配!

下一步

在本教程中,您学习了使用 ink!编程语言和属性宏创建智能合约的一些基本技术。

例如,本教程说明了:

  • 如何在新智能合约项目中添加存储项、指定数据类型和实现构造函数。
  • 如何向智能合约添加函数。
  • 如何向智能合约添加测试。
  • 如何使用cargo-contract上传和实例化合约。

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

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