开发智能合约

本教程指导你如何使用 ink! 编程语言 用于构建在基于 Substrate 的区块链上运行的智能合约。本节中的教程使用预配置的 contracts-node 和托管 Contracts UI。如果要使用标准 node template,需要将 Contracts pallet 和一些其他组件添加到开发环境中。在配置合约pallet中详细介绍了为构建智能合约准备 node template 的过程。

准备第一份合约

更新你的Rust环境

对于本节教程,你需要添加一些Rust源代码到你的Substrate开发环境之中,通过运行下面的命令更新你的Rust环境:

rustup component add rust-src --toolchain nightly

通过运行下面的命令验证你已经安装了WebAssembly目标:

rustup target add wasm32-unknown-unknown --toolchain nightly

如果目标已安装且是最新的,则该命令将显示类似于以下内容的输出:

info: component 'rust-std' for target 'wasm32-unknown-unknown' is up to date

安装Substrate合约node

为了简化本节教程,你可以为 Linux 和 macOS 下载预编译的 Substrate 节点,预编译的二进制文件默认情况下为智能合约包括了FRAME pallet。或者,你可以通过在你的本地计算机上运行 cargo install contracts-node 手动构建预配置的 contracts-node

在 macOS 或 Linux 上安装对应 Releases 版本的合约节点,为本地计算机下载合适的压缩文件。

如果无法下载预编译节点,则可以使用类似于以下的命令在本地编译它:

cargo install contracts-node --git https://github.com/paritytech/substrate-contracts-node.git --tag <latest-tag> --force --locked

你可以在 Tags 页面上发现最新的 tag。

安装其它软件包

编译 contracts-node 包后,你需要安装另外两个软件包:

  • 用于你的操作系统的 WebAssembly binaryen 包,为合约优化 WebAssembly 字节码。
  • 用于配置智能合约项目的 cargo-contract 命令行接口。

安装WebAssembly优化器

在 Ubuntu 或 Debian 上运行下面的命令:

sudo apt install binaryen

在 macOS 上运行下面的命令:

brew install binaryen

对于其它的操作系统,你可以直接从 WebAssembly releases 上下载 binaryen release。

安装cargo-contract包

在你安装 WebAssembly binaryen 包之后,你可以安装 cargo-contract 包。cargo-contract 包提供一个命令行接口,用于使用 ink! 语言处理智能合约。

安装 dylint-link,需要它来 lint ink! contracts,注意你使用 API 的方式可能存在会导致安全的问题:

cargo install dylint-link

通过运行下面的命令安装 cargo-contract

cargo install cargo-contract --force

通过运行以下命令验证安装并尝试命令的可用选项:

cargo contract --help

创建一个新的智能合约项目

你现在可以开始开发新的智能合约项目了,为智能合约项目生成文件。通过运行下面的命令创建一个名叫 flipper 的新项目文件夹。

cargo contract new flipper

切换到新项目文件夹中并列出文件夹中所有的内容:

cd flipper/ && ls -al

你应该可以看到文件夹中包含下面的文件:

-rwxr-xr-x   1 dev-doc  staff   285 Mar  4 14:49 .gitignore
-rwxr-xr-x   1 dev-doc  staff  1023 Mar  4 14:49 Cargo.toml
-rwxr-xr-x   1 dev-doc  staff  2262 Mar  4 14:49 lib.rs

像其它 Rust 项目一样,Cargo.tmol 文件被用作提供包的依赖关系和配置信息。lib.rs 文件被用作为智能合约编写业务逻辑。

浏览默认项目文件

默认情况下,创建一个新的智能合约项目生成一些模板源码。一个非常简单的合约,第一个函数 flip(),会将布尔变量从 true 更改为 false;还有第二个函数 get(),会获取当前布尔变量的值。lib.rs 文件还会包含两个为测试合约是否能按照期望工作的函数。

随着本节教程到最后,你会拥有一个更加高级完整的智能合约,像 Flipper example 一样。

打开 Cargo.toml 文件并为查看合约的依赖关系,如有必要,在 [dependencies] 部分,修改 scalescale-info 的配置:

scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] }
scale-info = { version = "2", default-features = false, features = ["derive"], optional = true }

保存对 Cargo.tmol 的任何变更,并关闭该文件。

打开 lib.rs 文件,并查看为合约定义的函数。

测试默认的合约

lib.rs 原代码文件的底部,有一些简单的测试用例可以验证合约的功能性。你可以使用链下测试环境来测试此代码是否按照预期执行。

通过运行下面的命令,使用 test 子命令和 nightly 工具链来为 flipper 合约执行默认的测试用例:

cargo +nightly test

该命令应显示类似于以下的输出,以表示成功完成测试用例:

running 2 tests
test flipper::tests::it_works ... ok
test flipper::tests::default_works ... ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

构建合约

测试完默认的合约之后,你就可以开始编译这个项目到 WebAssembly 了。 为智能合约构建 WebAssembly,通过运行下面的命令,编译 flipper 智能合约:

cargo +nightly contract build

这个命令会为 flipper 项目构建一个 WebAssembly 二进制文件,一个元数据文件可以包含合约应用二进制接口(ABI),和一个你用来部署合约的 .contract 文件。例如,你应该可以看到类似于下面的输出:

Original wasm size: 47.9K, Optimized: 22.8K

The contract was built in DEBUG mode.

Your contract artifacts are ready. You can find them in:
/Users/dev-doc/flipper/target/ink

- flipper.contract (code + metadata)
- flipper.wasm (the contract's code)
- metadata.json (the contract's metadata)
The `.contract` file can be used for deploying your contract to your chain.

target/ink 目录中的 metadata.json 文件描述了你可以用来与此合约交互的所有接口。这个文件包含了几个重要的部分:

  • spec 部分包含有关函数的信息,像可被调用的 constructorsmessages,可被发出的 events,以及可现实的任何文档。这个部分也包括了一个 selector 字段,它包含一个函数名称的4字哈希,和用于将合约调用到正确的函数。
  • storage 部分定义了由合约管理的所有存储项以及如何访问它们。
  • types 部分提供 JSON 其余部分中使用的自定义数据类型。

启动Substrate智能合约节点

通过运行下面的命令在本地开发模式中启动预配置的 contracts-node

substrate-contracts-node --dev

你应该在终端中看到类似于以下内容的输出:

2022-03-07 14:46:25 Substrate Contracts Node
2022-03-07 14:46:25 ✌️  version 0.8.0-382b446-x86_64-macos
2022-03-07 14:46:25 ❤️  by Parity Technologies <admin@parity.io>, 2021-2022
2022-03-07 14:46:25 📋 Chain specification: Development
2022-03-07 14:46:25 🏷  Node name: possible-plants-8517
2022-03-07 14:46:25 👤 Role: AUTHORITY
2022-03-07 14:46:25 💾 Database: RocksDb at /var/folders/2_/g86ns85j5l7fdnl621ptzn500000gn/T/substrateEdrJW9/chains/dev/db/full
2022-03-07 14:46:25 ⛓  Native runtime: substrate-contracts-node-100 (substrate-contracts-node-1.tx1.au1)
2022-03-07 14:46:25 🔨 Initializing Genesis block/state (state: 0xe9f1…4b89, header-hash: 0xa1b6…0194)
2022-03-07 14:46:25 👴 Loading GRANDPA authority set from genesis on what appears to be first startup.
2022-03-07 14:46:26 🏷  Local node identity is: 12D3KooWQ3P8BH7Z1C1ZoNSXhdGPCiPR7irRSeQCQMFg5k3W9uVd
2022-03-07 14:46:26 📦 Highest known block at #0

在几秒钟后,你还会看到区块被最终确认的状态。为了与区块链交互,你需要连接本节点。你可以通过打开 Contracts UI 浏览器页面连接当前节点。然后点击 Yes allow this application access,选择本地节点。

部署合约

在 Substrate 中,合约部署过程分为两个步骤:

  • 将合约代码上传到区块链。
  • 创建合约的一个实例。

通过这种模式,你可以将智能合约(如 ERC20标准)的代码存储在区块链上一次,然后将其实例化任意次数。你不需要重复加载相同的源代码,因此你的智能合约不会消耗区块链上不必要的资源。

上传合约代码

在本节教程中,你将使用 Contracts UI 前端在 Substrate 链上部署 flipper 合约。

在 web 浏览器中打开 Contracts UI,确认你已经连接到了 Local Node,点击 Add New Contract,点击 Upload New Contract Code,选择用于创建合约实例的 Account,你可以选择任何已存在的账户,包括一个预定义的账户,如 alice。输入一个智能合约的的名称,例如,Flipper Contract。

浏览并选择或拖放 flipper.contract 文件,包含绑定的 Wasm blob 和元数据文件到 upload section。

然后点击 Next 继续。

在区块链上创建一个实例

智能合约作为 Substrate 区块链上账户系统的扩展而存在。当你创建此智能合约的实例时,Substrate 将创建一个新的 AccountId,以存储智能合约管理的任何余额,并允许你与该合约进行交互。

在你上传智能合约并点击 Next 之后,Contracts UI 会显示有关智能合约内容的信息。

查看并接受智能合约初始版本的默认 Deployment Constructor 选项,查看并接受默认的 Max Gas Allowed 200000

点击 Next,交易现在已开始进入队列,如果需要进行更改,可以点击 Go Back 修改输入。

点击 Upload and Instantiate,根据你使用的帐户,可能会提示你输入帐户密码。如果你使用了预定义的帐户,则不需要提供密码。

调用智能合约

现在,你的合约已经部署在区块链上了,你可以与之交互。默认的 flipper 智能合约有两个函数 flip()get(),你可以使用 Contracts UI 来尝试使用它们。

get()函数

当你实例化合约时,将 flipper 合约 value 的初始值设置为 false。你可以使用 get() 函数去验证当前值是否为 false

为了测试 get() 函数,从 Account 列表中选择任意账户,此合约对允许谁发送这个 get() 请求没有限制,从 Message to Send 列表中选择 get(): bool,点击 Read,验证调用结果的返回值是否为 false

flip()函数

flip() 函数把值从 false 改为 true

为了测试 flip() 函数,从 Account 列表中选择任意预定义账户,flip() 函数是一个改变链状态的交易,需要一个有资金的帐户来执行调用。因此,你应该选择具有预定义余额的帐户,例如 alice 帐户。从 Message to Send 列表中选择 flip(),点击 Call,验证调用结果中的交易是否成功。

Message to Send 列表中选择 get(): bool,点击 Read,验证调用结果中的新值是 true

开发一份智能合约

在本节教程中,你将开发一个新的智能合约,它将在每次执行函数调用时增加计数器值。

智能合约与ink!

在上一节教程(准备第一份智能合约)之后,你已经位命令行访问 ink! 编程语言安装了 cargo-contract 包。ink! 语言是嵌入式领域特定语言。此语言使你能够使用 Rust 编程语言编写基于 WebAssembly 的智能合约。

该语言使用标准的 Rust 模式和专用的 #[ink(...)] 属性宏。这些属性宏描述了智能合约的不同部分,这样它们就可以被转换为与 Substrate 兼容的 WebAssembly 字节码。

创建一份新的智能合约项目

本节教程中,你将会为 incrementer 智能合约创建一个新项目。创建的新项目会添加一个项目文件夹,在项目文件夹中有一个默认的启动文件,也可以叫做模板文件。你将会为 incrementer 项目修改这些启动模板文件,来构建智能合约所需的逻辑。

通过运行下面的命令创建一个名为 incrementer 的新项目:

cargo contract new incrementer

进入 incrementer 文件夹后,打开 lib.rs 文件,默认情况下,lib.rs 模板文件包含 flipper 智能合约的源代码,只是把 flipper 合约的实例重命名为 incrementer。把默认模板的源代码替换为新的 incrementer 源码。保存 lib.rs 文件的改变并关闭。

打开 Cargo.toml 文件,并为合约检查相关的依赖包。在 [dependencies] 部分,如果需要的话,修改 scalescale-info 配置内容。

scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] }
scale-info = { version = "2", default-features = false, features = ["derive"], optional = true }

保存代码并关闭 Cargo.toml 文件。

通过运行下面的命令,验证程序是否编译并通过了简单的测试:

cargo +nightly 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

通过运行以下命令,验证你可以为合约构建 WebAssembly。

cargo +nightly contract build

如果程序成功编译,你就可以开始编码了。

存储简单的值

现在已经有了一些 incrementer 智能合约的初始源代码,你可以再引入一些新的功能。例如,该合约需要存储简单的值。下面的代码解释了如何使用 #[ink(storage)] 属性宏为该合约存储简单的值。

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

支持的类型

Substrate 智能合约支持最常见的 Rust 数据类型,包括布尔类型,未签名和已签名的整数型,字符串,元组,以及数组。这些数据类型使用 Parity scale codec 进行编码和解码,以便可以在网络上高效的传输。

除了常见的 Rust 类型使用 scale codec 进行编码解码外,ink! 语言支持 Substrate 特性类型,像 AccountIdBalance,和 Hash,就好像它们是原始类型。下面的代码说明了如何为该合约存储一个 AccountIdBalance

// We are importing the default ink! types
use ink_lang as ink;

#[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! 智能合约必须至少有一个在创建合约时运行的构造函数。然而,如果你需要,一个智能合约也可以有多个多个构造函数。下面的代码说明了如何使用多个构造函数:

use ink_lang as ink;

#[ink::contract]
mod mycontract {

	#[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.
		///
		/// 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

为了更新智能合约,打开 lib.rs 文件,通过声明名为 value 数据类型为 i32 的存储项,从而替换 Storage Declaration 注释。

#[ink(storage)]
pub struct Incrementer {
   value: i32,
}

修改 Incrementer 构造函数,将其 value 设置为 init_value

impl Incrementer {
 #[ink(constructor)]
 pub fn new(init_value: i32) -> Self {
       Self {
         value: init_value,
       }
 }
}

添加第二个名为 default 的构造函数,该函数创建一个新的 Incrementer,将其 value 设置为 0

#[ink(constructor)]
pub fn default() -> Self {
   Self {
       value: 0,
   }
}

保存代码并关闭文件。

通过运行下面的命令使用 test 子命令和 nightly 工具链来测试你的工作。

cargo +nightly 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

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

现在你已经创建并初始化了存储值,可以使用公共和私有函数与它进行交互。在本节教程中,你将添加一个公共函数来获取存储值。请注意,所有公共函数都必须使用 #[ink(message)] 属性宏。

在智能合约中添加公共函数,打开 lib.rs 文件,更新 get 公共函数,以返回数据类型为 i32 的存储项的 value

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

因为这个函数只从合约存储中读取数据,所以它使用 &self 参数来访问合约函数和存储项,此函数不允许更改存储项 value 的状态。如果函数中的最后一个表达式没有分号(;),Rust 将则将其视为返回值。

将私有 default_works 函数中的 Test Your Contract 注释替换为测试 get 函数的代码。

fn default_works() {
   let contract = Incrementer::default();
   assert_eq!(contract.get(), 0);
}

保存代码并关闭文件。

通过运行下面的命令使用 test 子命令和 nightly 工具链来测试你的工作。

cargo +nightly test

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

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

添加一个函数以增加存储项的值,打开 lib.rs 文件,添加一个新的 inc 公共函数以增加 value 存储,使用数据类型是 i32by 参数

#[ink(message)]
pub fn inc(&mut self, by: i32) {
   self.value += by;
}

向源代码中添加一个新测试,以验证此函数。

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

保存代码并关闭文件。

通过运行下面的命令使用 test 子命令和 nightly 工具链来测试你的工作。

cargo +nightly 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 后,你可以使用 Contracts UI 在本地合约节点上部署和测试智能合约。

为智能合约构建WebAssembly,在 incrementer 项目文件夹里,通过运行下面的命令编译 incrementer 智能合约:

cargo +nightly 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)
- metadata.json (the contract's metadata)

部署与测试智能合约

如果你已经在本地安装了 substrate-contracts-node 节点,你可以为智能合约启动一个本地区块链节点,然后使用 Contracts UI 去部署和测试智能合约。

通过运行以下命令,在本地开发模式下启动合约节点:

substrate-contracts-node --dev

打开 Contracts UI 并验证其是否连接到本地节点。 点击 Add New Contract。 点击 Upload New Contract Code。 选择 incrementer.contract 文件,然后点击 Next。 点击 Upload and Instantiate。 使用 Contracts UI 探索智能合约并与之交互。

完整的合约代码:


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

use ink_lang as ink;

#[ink::contract]
mod incrementer {

    #[ink(storage)]
    pub struct Incrementer {
        value: i32,
    }

    impl Incrementer {
        #[ink(constructor)]
        pub fn new(init_value: i32) -> Self {
            Self {
                value: init_value,
            }
        }

        #[ink(constructor)]
        pub fn default() -> Self {
            Self {
                value: Default::default(),
            }
        }

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

        #[ink(message)]
        pub fn inc(&mut self, by: i32) {
            self.value += by;
        }
    }

    #[cfg(test)]
    mod tests {
        use super::*;
        use ink_lang as ink;

        #[ink::test]
        fn default_works() {
            let contract = Incrementer::default();
            assert_eq!(contract.get(), 0);
        }

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

使用maps存储值

本节教程说明了如何可以扩展智能合约的功能,以管理每个用户的一个号码。要添加这个功能,你将会使用 Mapping 类型。

ink! 语言提供 Mapping 类型,使你能够将数据存储为键值对。例如,下面的代码说明了映射一个用户到一个号码。

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

使用 Mapping 数据类型,可以为每个键存储值的唯一实例。本节教程中,每一个 AccountId 代表一个键,该键映射到一个且仅一个存储的数字 my_value。每个用户只能存储、增加和检索与其自己的 AccountId 关联的值。

初始化一个mapping

第一步是初始化 AccountId 和存储值之间的映射。在你的合约中使用映射之前,你必须始终初始化映射,以避免映射错误和不一致。要初始化映射,需要执行以下操作:

以下示例说明如何初始化一个映射并检索一个值:

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

use ink_lang as ink;

#[ink::contract]
mod mycontract {
    use ink_storage::traits::SpreadAllocate;

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

    impl MyContract {
        #[ink(constructor)]
        pub fn new(count: u32) -> Self {
            // This call is required to correctly initialize the
            // Mapping of the contract.
            ink_lang::utils::initialize_contract(|contract: &mut Self| {
                let caller = Self::env().caller();
                contract.map.insert(&caller, &count);
            })
        }

        #[ink(constructor)]
        pub fn default() -> Self {
            ink_lang::utils::initialize_contract(|_| {})
        }

        // 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.map.get(&caller).unwrap_or_default()
        }
    }
}

识别合约的调用者

在前面的例子中,你可能注意到了 self.env().caller() 函数调用。这个函数在整个合约逻辑中都是可用的,并且总是返回 contract caller。重要的是要注意,contract callerorigin caller 是不同的。如果用户访问一个合约,然后再调用后来的合约,那么第二个合约中的 self.env().caller() 是第一个合约的地址,而不是 original user 的地址。

使用合约的调用者

在许多情况下,合约调用者都是有用的。例如,你可以使用 self.env().caller() 来创建一个访问控制层,该层只允许用户访问自己的值。你还可以使用 self.env().caller() 在合同部署期间保存合约所有者。例如:

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

use ink_lang as ink;

#[ink::contract]
mod mycontract {

	#[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 标识符保存了合约调用者,所以你可以再后面编写函数来检查当前合约调用者是否是合同的所有者。

添加mapping到智能合约中

incrementer 合约添加 storage map,在 incrementer 项目文件夹中,打开 lib.rs,为你的合约导入 SpreadAllocate 特征并追加派生宏。

#[ink::contract]
mod incrementer {
   use ink_storage::traits::SpreadAllocate;

   #[ink(storage)]
   #[derive(SpreadAllocate)]

将映射键从 AccountId 添加到存储为 my_valuei32 数据类型。

pub struct Incrementer {
   value: i32,
   my_value: ink_storage::Mapping<AccountId, i32>,
}

使用 initialize_contract 函数为合约中的 new 函数设置一个初始 valuemy_value

#[ink(constructor)]
pub fn new(init_value: i32) -> Self {
   ink_lang::utils::initialize_contract(|contract: &mut Self| {
       contract.value = init_value;
       let caller = Self::env().caller();
       contract.my_value.insert(&caller, &0);
   })
}

使用 initialize_contract 函数为合约中的 default 函数设置一个初始 value

#[ink(constructor)]
pub fn default() -> Self {
   ink_lang::utils::initialize_contract(|contract: &mut Self| {
       contract.value = Default::default();
   })
}

添加一个 get_mine 函数,使用 Mapping API get 函数读取 my_value,并为合约调用者返回 my_value

#[ink(message)]
pub fn get_mine(&self) -> i32 {
   self.my_value.get(&self.env().caller()).unwrap_or_default()
}

向初始化帐户添加一个新测试。

#[ink::test]
fn my_value_works() {
   let contract = Incrementer::new(11);
   assert_eq!(contract.get(), 11);
   assert_eq!(contract.get_mine(), 0);
}

保存代码并关闭文件。

通过运行下面的命令使用 test 子命令和 nightly 工具链来测试你的工作。

cargo +nightly test

该命令应该显示类似如下的输出,以表明测试成功完成:

running 3 tests
test incrementer::tests::default_works ... ok
test incrementer::tests::it_works ... ok
test incrementer::tests::my_value_works ... ok

test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

插入,更新或删除值

ink_storage 映射提供对存储项的直接访问。例如,你可以用一个已存在的键调用 Mapping::insert() 来替换存储项之前保存的值。你也可以通过使用 Mapping::insert() 在存储中第一次读取值时,然后用 Mapping::insert() 更新这个值。如果不存在给定键的值,则 Mapping::get() 返回 None。因为 Mapping API 提供了对存储的直接访问,所以可以使用 Mapping::remove() 方法从存储中删除给定键的值。

要添加插入和删除函数到合约中,在 incrementer 项目文件夹中,打开 lib.rs 文件,添加一个 inc_mine() 函数,它允许合约调用者获取 my_value 存储项,并且在映射中插入递增的 value

#[ink(message)]
pub fn inc_mine(&mut self, by: i32) {
   let caller = self.env().caller();
   let my_value = self.get_mine();
   self.my_value.insert(caller, &(my_value + by));
}

添加一个 remove_mine() 函数,该函数允许合约调用者从存储中获取可删除的 my_value 存储项。

#[ink(message)]
pub fn remove_mine(&mut self) {
   self.my_value.remove(&self.env().caller())
}

添加一个新测试,以验证 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);
}

通过运行下面的命令使用 test 子命令和 nightly 工具链来测试你的工作。

cargo +nightly 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_value_works ... ok

test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

完整的代码如下:

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

use ink_lang as ink;

#[ink::contract]
mod incrementer {
    use ink_storage::traits::SpreadAllocate;

    #[ink(storage)]
    #[derive(SpreadAllocate)]
    pub struct Incrementer {
        value: i32,
        my_value: ink_storage::Mapping<AccountId, i32>,
    }

    impl Incrementer {
        #[ink(constructor)]
        pub fn new(init_value: i32) -> Self {
            // This call is required in order to correctly initialize the
            // `Mapping`s of our contract.
            ink_lang::utils::initialize_contract(|contract: &mut Self| {
                contract.value = init_value;
                let caller = Self::env().caller();
                contract.my_value.insert(&caller, &0);
            })
        }

        #[ink(constructor)]
        pub fn default() -> Self {
            // Even though we're not explicitly initializing the `Mapping`,
            // we still need to call this
            ink_lang::utils::initialize_contract(|contract: &mut Self| {
                contract.value = Default::default();
            })
        }

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

        #[ink(message)]
        pub fn inc(&mut self, by: i32) {
            self.value += by;
        }

        #[ink(message)]
        pub fn get_mine(&self) -> i32 {
            self.my_value.get(&self.env().caller()).unwrap_or_default()
        }

        #[ink(message)]
        pub fn inc_mine(&mut self, by: i32) {
            let caller = self.env().caller();
            let my_value = self.get_mine();
            self.my_value.insert(caller, &(my_value + by));
        }

        #[ink(message)]
        pub fn remove_mine(&self) {
            self.my_value.remove(&self.env().caller())
        }
    }

    #[cfg(test)]
    mod tests {
        use super::*;

        use ink_lang as ink;

        #[ink::test]
        fn default_works() {
            let contract = Incrementer::default();
            assert_eq!(contract.get(), 0);
        }

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

        #[ink::test]
        fn my_value_works() {
            let mut contract = Incrementer::new(11);
            assert_eq!(contract.get(), 11);
            assert_eq!(contract.get_mine(), 0);
            contract.inc_mine(5);
            assert_eq!(contract.get_mine(), 5);
            contract.inc_mine(10);
            assert_eq!(contract.get_mine(), 15);
        }

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

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

构建一个token合约

本节教程演示了如何使用 ink! 语言构建 ERC-20 代币合约,ERC-20 规范定义了可替换代币的通用标准。拥有定义代币属性的标准,使遵循规范的开发人员,能够构建与其他产品和服务互操作的应用程序。ERC-20 代币标准不是唯一的代币标准,但它是最常用的标准之一。

ERC-20标准的基础知识

ERC-20 代币标准定义了运行在以太坊区块链上的大多数智能合约的接口。这些标准接口允许个人在现有的智能合约平台上部署自己的加密货币。

如果你查看该标准,你会发现有如下被定义的核心函数:

// ----------------------------------------------------------------------------
// ERC Token Standard #20 Interface
// https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20.md
// ----------------------------------------------------------------------------

contract ERC20Interface {
    // Storage Getters
    function totalSupply() public view returns (uint);
    function balanceOf(address tokenOwner) public view returns (uint balance);
    function allowance(address tokenOwner, address spender) public view returns (uint remaining);

    // Public Functions
    function transfer(address to, uint tokens) public returns (bool success);
    function approve(address spender, uint tokens) public returns (bool success);
    function transferFrom(address from, address to, uint tokens) public returns (bool success);

    // Contract Events
    event Transfer(address indexed from, address indexed to, uint tokens);
    event Approval(address indexed tokenOwner, address indexed spender, uint tokens);
}

用户余额映射到帐户地址,接口允许用户转移他们拥有的代币或允许第三方代表他们转移代币。最重要的是,必须实现智能合约逻辑,以确保资金不会被无意创建或销毁,并保护用户的资金免受恶意参与者的攻击。请注意,所有公共函数都返回一个 bool 值,该 bool 值仅指示调用是否成功。在Rust中,这些函数通常会返回一个 Result

创建token供应

处理 ERC-20 代币的智能合约类似于上一节教程使用maps存储值的 Incrementer 合约。对于本教程,ERC-20 合约由固定的代币供应组成,当部署合同时,这些代币全部存入与合同所有者相关联的帐户。然后,合同所有者可以将代币分发给其他用户。在本教程中创建的简单 ERC-20 合约并不是制造和分发代币的唯一方法。然而,这个 ERC-20 合约为扩展你在其他教程中所学到的知识,以及如何使用 ink! 语言用于构建更健壮的智能合约,提供了一个良好的基础。

对于 ERC-20 代币合约,初始存储包括:

  • total_supply 代表合约中代币的总供应量。
  • balances 表示每个账户的个人余额。

要构建一个 ERC-20 代币智能合约,通过运行以下命令创建一个名为 erc20 的新项目:

cargo contract new erc20

erc20 目录中打开 lib.rs 文件,使用新 erc20 源码替换默认的模板源码。

保存代码改变并关闭文件。

打开 Cargo.toml 文件,并检查合约的依赖关系。

如有必要,在 [dependencies] 部分,修改 scalescale-info 的配置:

scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] }
scale-info = { version = "2", default-features = false, features = ["derive"], optional = true }

保存代码改变并关闭文件。

通过运行下面的命令,验证程序是否编译并通过了简单的测试:

cargo +nightly test

该命令应该显示类似如下的输出,以表明测试成功完成:

running 2 tests
test erc20::tests::new_works ... ok
test erc20::tests::balance_works ... ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

通过运行以下命令,验证你可以为合约构建 WebAssembly。

cargo +nightly contract build

如果程序编译成功,您就可以在当前状态上传它,或者开始向合约添加功能。

上传并实例化合约

如果你想测试到目前为止所有的内容,你可以使用 Contracts UI 上传该合约。

在添加新功能之前测试 ERC-20 合同:

  1. 启动本地合约节点。
  2. 上传 erc20.contract 文件。
  3. new 构造函数指定代币的初始供应。
  4. 在运行的本地节点上实例化合同。
  5. 选择 totalSupply 作为要发送的消息,然后点击 Read 以验证代币的总供应与初始供应相同。
  6. 选择 balanceOf 作为要发送的消息。
  7. 选择用于实例化合约的帐户 AccountId,然后点击 Read。 如果选择任何其他 AccountId,则点击 Read,余额为零,因为所有代币均由合约所有者所有。

转移tokens

此时,ERC-20 合约有一个用户帐户,该帐户拥有该合约代币的 total_supply。为了使该合约有用,合约所有者必须能够将代币转移到其他帐户。对于这个简单的 ERC-20 合约,你将添加一个公共 transfer 函数,使你作为合约调用者能够将你拥有的代币转移给另一个用户。

公共 transfer 函数调用私有 transfer_from_to() 函数。因为这是一个内部函数,所以不需要任何授权检查就可以调用它。但是,转账逻辑必须能够确定 from 帐户是否具有可转移到接收方 to 帐户的代币数量。transfer_from_to() 函数使用合约调用者(self.env().caller())作为 from 帐户。在此上下文中,transfer_from_to() 函数执行以下操作:

  • 获取 from 帐户和 to 帐户的当前余额。
  • 检查 from 余额是否小于要发送的代币的 value
let from_balance = self.balance_of(from);
if from_balance < value {
return Err(Error::InsufficientBalance)
}

从转移账户中减去该值,并将该值添加到接收账户。

self.balances.insert(from, &(from_balance - value));
let to_balance = self.balance_of(to);
self.balances.insert(to, &(to_balance + value));

要添加转移函数到智能合约,在 erc20 项目目录中,打开 lib.rs 文件,添加一个 Error 声明,以在帐户中没有足够的代币来完成转账时返回一个错误。

/// Specify ERC-20 error type.
#[derive(Debug, PartialEq, Eq, scale::Encode, scale::Decode)]
#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))]
pub enum Error {
/// Return if the balance cannot fulfill a request.
   InsufficientBalance,
}

添加一个 Result 返回类型,以返回 InsufficientBalance 错误。

/// Specify the ERC-20 result type.
pub type Result<T> = core::result::Result<T, Error>;

添加 transfer() 公共函数,使合约调用者能够将代币转移到另一个帐户。

#[ink(message)]
pub fn transfer(&mut self, to: AccountId, value: Balance) -> Result<()> {
   let from = self.env().caller();
   self.transfer_from_to(&from, &to, value)
}

添加 transfer_from_to() 私有函数,将代币从与合约调用者关联的帐户转移到接收帐户。

fn transfer_from_to(
   &mut self,
   from: &AccountId,
   to: &AccountId,
   value: Balance,
) -> Result<()> {
    let from_balance = self.balance_of_impl(from);
    if from_balance < value {
        return Err(Error::InsufficientBalance)
    }

    self.balances.insert(from, &(from_balance - value));
    let to_balance = self.balance_of_impl(to);
    self.balances.insert(to, &(to_balance + value));
    Ok(())
}

这段代码片段使用了 balance_of_impl() 函数,balance_of_impl() 函数与 balance_of 函数相同,不同的是它使用引用在 WebAssembly 中以更有效的方式查找帐户余额。在智能合约中添加如下函数即可使用该功能:

#[inline]
fn balance_of_impl(&self, owner: &AccountId) -> Balance {
   self.balances.get(owner).unwrap_or_default()
}

通过运行下面的命令,验证程序是否编译并通过了测试用例:

cargo +nightly test

该命令应该显示类似如下的输出,以表明测试成功完成:

running 3 tests
test erc20::tests::new_works ... ok
test erc20::tests::balance_works ... ok
test erc20::tests::transfer_works ... ok

test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

创建事件

ERC-20 代币标准规定合约调用在提交一个交易时不能直接返回值。然而,你可能希望你的智能合约以某种方式发出已经发生事件的信号。例如,你可能希望你的智能合约显示交易完成的时间或转账通过的时间,你可以使用事件来发送这些类信号。你可以使用事件与任何类型的数据进行通信,为一个事件定义数据类似于定义 struct。事件应该使用 #[ink(event)] 属性被声明。

添加一个转移事件

声明一个 Transfer 事件,以提供有关已完成转移操作的信息。Transfer 事件包含以下信息:

  • Balance 类型的值。
  • from 帐户的一个 Option-wrapped AccountId 变量。
  • to 帐户的一个 Option-wrapped AccountId 变量。

为了更快地访问事件数据,可以使用索引字段。你可以通过在该字段上使用 #[ink(topic)] 属性标签来实现这一点。

要添加一个 Transfer 事件,打开 lib.rs 文件,使用 #[ink(event)] 属性宏声明事件。

#[ink(event)]
pub struct Transfer {
   #[ink(topic)]
   from: Option<AccountId>,
   #[ink(topic)]
   to: Option<AccountId>,
   value: Balance,
 }

你可以使用 .unwrap_or() 函数为 Option<T> 变量检索数据。

发出事件

现在你已经声明了事件并定义了事件包含的信息,接下来需要添加发出事件的代码。你可以通过调用 self.env().emit_event() 来实现这一点,将事件名称作为调用的唯一参数。

在这个 ERC-20 合约中,你希望在每次发生转移时发出一个 Transfer 事件,代码中有两个地方出现了这种情况

  • 在初始化合约的 new 调用期间。
  • 每次调用 transfer_from_to

fromto 字段的值是 Option<AccountId> 数据类型。然而,在 tokens 的初始转移过程中,为 initialsupply 设置的值不是来自任何其他帐户。在本例中,Transfer 事件的 from 值为 None

要发出 Transfer 事件,打开 lib.rs 文件,在 new 构造函数中将 Transfer 事件添加到 new_init() 函数中。

fn new_init(&mut self, initial_supply: Balance) {
   let caller = Self::env().caller();
   self.balances.insert(&caller, &initial_supply);
   self.total_supply = initial_supply;
   Self::env().emit_event(Transfer {
       from: None,
       to: Some(caller),
       value: initial_supply,
     });
   }

Transfer 事件添加到 transfer_from_to() 函数中。

self.balances.insert(from, &(from_balance - value));
let to_balance = self.balance_of_impl(to);
self.balances.insert(to, &(to_balance + value));
self.env().emit_event(Transfer {
   from: Some(*from),
   to: Some(*to),
   value,
});

注意 value 不需要 Some(),因为值没有存储在 Option 中。

添加一个将代币从一个帐户转移到另一个帐户的测试用例。

#[ink::test]
fn transfer_works() {
   let mut erc20 = Erc20::new(100);
   assert_eq!(erc20.balance_of(AccountId::from([0x0; 32])), 0);
   assert_eq!(erc20.transfer((AccountId::from([0x0; 32])), 10), Ok(()));
   assert_eq!(erc20.balance_of(AccountId::from([0x0; 32])), 10);
}

通过运行以下命令验证程序的编译并且通过所有测试用例:

cargo +nightly test

该命令应该显示类似以下的输出,以表示成功完成测试:

running 3 tests
test erc20::tests::new_works ... ok
test erc20::tests::balance_works ... ok
test erc20::tests::transfer_works ... ok

test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

启用第三方转账

ERC-20 代币合约现在可以在账户之间转移代币,并在发生这种情况时发出事件。作为最后一步,你可以添加 approvetransfer_from 函数以启用第三方转账。

允许一个账户代表另一个账户消费代币,允许你的智能合约支持去中心化的交易所。你可以批准你所拥有的一些代币以你的名义进行交易,而不是在合同中直接将你的代币转让给另一个用户。当你等待交易执行时,如果需要,你仍然可以控制和使用你的代币。你还可以批准多个合约或用户访问你的代币,因此,如果一个合约提供了最好的交易,你不需要将代币从一个合约移动到另一个合约,这可能是一个高成本和耗时的过程。

为了确保批准和转移可以安全完成,ERC-20 代币合约使用两步流程,分别从操作中进行批准和转移。

添加批准逻辑

批准另一个帐户来使用你的代币是第三方转账流程的第一步。作为一个代币所有者,你可以指定任何帐户代表转账,以及转账任意数量的代币。你不需要批准帐户中的所有令牌,你可以指定批准一个帐户允许转账的最大数量。

当多次调用 approve 时,将用新值覆盖先前批准的值。默认情况下,任意两个帐户之间的批准值为 0。如果你想撤销帐户中对代币的访问,可以调用值为 0approve 函数。

要在 ERC-20 合约中存储批准,你需要使用稍微复杂一些的 Mapping 键。由于每个帐户可以有不同的批准金额供其他任何帐户使用,因此需要使用元组作为映射到余额值的键。例如:

pub struct Erc20 {
 /// Balances that can be transferred by non-owners: (owner, spender) -> allowed
 allowances: ink_storage::Mapping<(AccountId, AccountId), Balance>,
}

元组使用 (owner, spender) 来标识 spender 帐户,该帐户被允许在指定 allowance 代表 owner 访问代币。

要为智能合约添加批准逻辑,打开 lib.rs 文件,使用 #[ink(event)] 属性宏声明 Approval 事件。

#[ink(event)]
pub struct Approval {
   #[ink(topic)]
   owner: AccountId,
   #[ink(topic)]
   spender: AccountId,
   value: Balance,
}

添加一个 Error 声明,以便在转账请求超过帐户限额时返回一个错误。

#[derive(Debug, PartialEq, Eq, scale::Encode, scale::Decode)]
#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))]
pub enum Error {
   InsufficientBalance,
   InsufficientAllowance,
}

将所有者和非所有者组合的存储映射添加到帐户余额。

allowances: Mapping<(AccountId, AccountId), Balance>,

添加 approve() 函数来授权 spender 帐户从调用者的帐户中提取代币,直到取到最大 value

#[ink(message)]
pub fn approve(&mut self, spender: AccountId, value: Balance) -> Result<()> {
   let owner = self.env().caller();
   self.allowances.insert((&owner, &spender), &value);
   self.env().emit_event(Approval {
     owner,
     spender,
     value,
   });
   Ok(())
}

添加一个 allowance() 函数来返回允许 spenderowner 帐户中提取的代币数量。

#[ink(message)]
pub fn allowance(&self, owner: AccountId, spender: AccountId) -> Balance {
   self.allowance_impl(&owner, &spender)
}

这段代码片段使用了 allowance_impl() 函数。allowance_impl() 函数与 allowance 函数相同,不同之处是它使用引用来在 WebAssembly 中以更有效的方式查找代币限额。在智能合约中添加如下函数即可使用该功能:

#[inline]
fn allowance_impl(&self, owner: &AccountId, spender: &AccountId) -> Balance {
   self.allowances.get((owner, spender)).unwrap_or_default()
}

从逻辑中添加转账

现在你已经为一个帐户设置了代表另一个帐户转移代币的批准,你需要创建一个 transfer_from 函数,以允许已批准的用户转移代币。transfer_from 函数调用私有 transfer_from_to 函数来执行大部分转移逻辑。授权非所有者转让代币有几个要求:

  • self.env().caller() 合约调用者分配的必须是 from 帐户中可用的代币。
  • 分配的存储作为 allowance 必须大于要转移的值。

如果满足了这些要求,合约将更新的限额插入到 allowance 变量中,并使用指定的 fromto 帐户调用 transfer_from_to() 函数。

记住,在调用 transfer_from 时,self.env().caller()from 帐户用于查找当前限额,但 transfer_from 函数是在指定的 fromto 帐户之间被调用的。

每当调用 transfer_from 时,将使用三个帐户变量,你需要确保正确使用它们。

要为智能合约添加 transfer_from,打开 lib.rs 文件,添加 transfer_from() 函数,以代表 from 帐户到 to 帐户转移代币的 value

/// Transfers tokens on the behalf of the `from` account to the `to account
#[ink(message)]
pub fn transfer_from(
   &mut self,
   from: AccountId,
   to: AccountId,
   value: Balance,
) -> Result<()> {
   let caller = self.env().caller();
   let allowance = self.allowance_impl(&from, &caller);
   if allowance < value {
       return Err(Error::InsufficientAllowance)
   }
   self.transfer_from_to(&from, &to, value)?;
   self.allowances
       .insert((&from, &caller), &(allowance - value));
   Ok(())
  }

transfer_from() 函数添加一个测试用例。

#[ink::test]
fn transfer_from_works() {
   let mut contract = Erc20::new(100);
   assert_eq!(contract.balance_of(AccountId::from([0x1; 32])), 100);
   contract.approve(AccountId::from([0x1; 32]), 20);
   contract.transfer_from(AccountId::from([0x1; 32]), AccountId::from([0x0; 32]), 10);
   assert_eq!(contract.balance_of(AccountId::from([0x0; 32])), 10);
}

通过运行以下命令验证程序的编译并且通过所有测试用例:

cargo +nightly test

该命令应显示类似如下的输出,以表示测试成功完成:

running 5 tests
test erc20::tests::new_works ... ok
test erc20::tests::balance_works ... ok
test erc20::tests::transfer_works ... ok
test erc20::tests::transfer_from_works ... ok
test erc20::tests::allowances_works ... ok

test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

通过运行以下命令验证你可以为该合约构建 WebAssemnbly

cargo +nightly contract build

在构建了合约的 WebAssembly 之后,你可以使用 Contracts UI 上传并实例化它,如上传并实例化合约中所述。

编写测试用例

在本节教程中,你向 lib.rs 文件中添加了简单的单元测试。基本的测试用例通过给出指定的输入值并验证返回的结果来说明函数按预期工作。你可以通过编写额外的测试用例来提高代码的质量。例如,你可以添加测试,对无效输入、空值或超出预期范围的值进行错误处理。

erc20 最终代码:

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

use ink_lang as ink;

#[ink::contract]
mod erc20 {
    use ink_storage::traits::SpreadAllocate;

    #[ink(event)]
    pub struct Transfer {
        #[ink(topic)]
        from: Option<AccountId>,
        #[ink(topic)]
        to: Option<AccountId>,
        #[ink(topic)]
        value: Balance,
    }

    #[ink(event)]
    pub struct Approval {
        #[ink(topic)]
        owner: AccountId,
        #[ink(topic)]
        spender: AccountId,
        #[ink(topic)]
        value: Balance,
    }

    #[cfg(not(feature = "ink-as-dependency"))]
    #[ink(storage)]
    #[derive(SpreadAllocate)]
    pub struct Erc20 {
        /// The total supply.
        total_supply: Balance,
        /// The balance of each user.
        balances: ink_storage::Mapping<AccountId, Balance>,
        /// Approval spender on behalf of the message's sender.
        allowances: ink_storage::Mapping<(AccountId, AccountId), Balance>,
    }

    impl Erc20 {
        #[ink(constructor)]
        pub fn new(initial_supply: Balance) -> Self {
            ink_lang::utils::initialize_contract(|contract: &mut Self| {
                contract.total_supply = initial_supply;
                let caller = Self::env().caller();
                contract.balances.insert(&caller, &initial_supply);

                // NOTE: `allowances` is default initialized by `initialize_contract`, so we don't
                // need to do anything here

                Self::env().emit_event(Transfer {
                    from: None,
                    to: Some(caller),
                    value: initial_supply,
                });
            })
        }

        #[ink(message)]
        pub fn total_supply(&self) -> Balance {
            self.total_supply
        }

        #[ink(message)]
        pub fn balance_of(&self, owner: AccountId) -> Balance {
            self.balances.get(&owner).unwrap_or_default()
        }

        #[ink(message)]
        pub fn approve(&mut self, spender: AccountId, value: Balance) -> bool {
            // Record the new allowance.
            let owner = self.env().caller();
            self.allowances.insert(&(owner, spender), &value);

            // Notify offchain users of the approval and report success.
            self.env().emit_event(Approval {
                owner,
                spender,
                value,
            });

            true
        }

        #[ink(message)]
        pub fn allowance(&self, owner: AccountId, spender: AccountId) -> Balance {
            self.allowance_of_or_zero(&owner, &spender)
        }

        #[ink(message)]
        pub fn transfer_from(
            &mut self,
            from: AccountId,
            to: AccountId,
            value: Balance,
        ) -> bool {
            // Ensure that a sufficient allowance exists.
            let caller = self.env().caller();
            let allowance = self.allowance_of_or_zero(&from, &caller);
            if allowance < value {
                return false
            }

            let transfer_result = self.transfer_from_to(from, to, value);
            // Check `transfer_result` because `from` account may not have enough balance
            //   and return false.
            if !transfer_result {
                return false
            }

            // Decrease the value of the allowance and transfer the tokens.
            self.allowances.insert((from, caller), &(allowance - value));
            true
        }

        #[ink(message)]
        pub fn transfer(&mut self, to: AccountId, value: Balance) -> bool {
            self.transfer_from_to(self.env().caller(), to, value)
        }

        fn transfer_from_to(
            &mut self,
            from: AccountId,
            to: AccountId,
            value: Balance,
        ) -> bool {
            let from_balance = self.balance_of(from);
            if from_balance < value {
                return false
            }

            // Update the sender's balance.
            self.balances.insert(&from, &(from_balance - value));

            // Update the receiver's balance.
            let to_balance = self.balance_of(to);
            self.balances.insert(&to, &(to_balance + value));

            self.env().emit_event(Transfer {
                from: Some(from),
                to: Some(to),
                value,
            });

            true
        }

        fn allowance_of_or_zero(
            &self,
            owner: &AccountId,
            spender: &AccountId,
        ) -> Balance {
            // If you are new to Rust, you may wonder what's the deal with all the asterisks and
            // ampersends.
            //
            // In brief, using `&` if we want to get the address of a value (aka reference of the
            // value), and using `*` if we have the reference of a value and want to get the value
            // back (aka dereferencing).
            //
            // To read more: https://doc.rust-lang.org/book/ch04-02-references-and-borrowing.html
            self.allowances.get(&(*owner, *spender)).unwrap_or_default()
        }
    }

    #[cfg(test)]
    mod tests {
        use super::*;

        use ink_lang as ink;

        #[ink::test]
        fn new_works() {
            let contract = Erc20::new(777);
            assert_eq!(contract.total_supply(), 777);
        }

        #[ink::test]
        fn balance_works() {
            let contract = Erc20::new(100);
            assert_eq!(contract.total_supply(), 100);
            assert_eq!(contract.balance_of(AccountId::from([0x1; 32])), 100);
            assert_eq!(contract.balance_of(AccountId::from([0x0; 32])), 0);
        }

        #[ink::test]
        fn transfer_works() {
            let mut contract = Erc20::new(100);
            assert_eq!(contract.balance_of(AccountId::from([0x1; 32])), 100);
            assert!(contract.transfer(AccountId::from([0x0; 32]), 10));
            assert_eq!(contract.balance_of(AccountId::from([0x0; 32])), 10);
            assert!(!contract.transfer(AccountId::from([0x0; 32]), 100));
        }

        #[ink::test]
        fn transfer_from_works() {
            let mut contract = Erc20::new(100);
            assert_eq!(contract.balance_of(AccountId::from([0x1; 32])), 100);
            contract.approve(AccountId::from([0x1; 32]), 20);
            contract.transfer_from(
                AccountId::from([0x1; 32]),
                AccountId::from([0x0; 32]),
                10,
            );
            assert_eq!(contract.balance_of(AccountId::from([0x0; 32])), 10);
        }

        #[ink::test]
        fn allowances_works() {
            let mut contract = Erc20::new(100);
            assert_eq!(contract.balance_of(AccountId::from([0x1; 32])), 100);
            contract.approve(AccountId::from([0x1; 32]), 200);
            assert_eq!(
                contract
                    .allowance(AccountId::from([0x1; 32]), AccountId::from([0x1; 32])),
                200
            );

            assert!(contract.transfer_from(
                AccountId::from([0x1; 32]),
                AccountId::from([0x0; 32]),
                50
            ));
            assert_eq!(contract.balance_of(AccountId::from([0x0; 32])), 50);
            assert_eq!(
                contract
                    .allowance(AccountId::from([0x1; 32]), AccountId::from([0x1; 32])),
                150
            );

            assert!(!contract.transfer_from(
                AccountId::from([0x1; 32]),
                AccountId::from([0x0; 32]),
                100
            ));
            assert_eq!(contract.balance_of(AccountId::from([0x0; 32])), 50);
            assert_eq!(
                contract
                    .allowance(AccountId::from([0x1; 32]), AccountId::from([0x1; 32])),
                150
            );
        }
    }
}

智能合约的问题排查

本节介绍了在基于 Substrate 的区块链上编写和部署智能合约时可能遇到的一些常见问题,以及如何解决这些问题。

意外的运行错误

如果你在不正确的情况下中断了一个正在运行的节点,例如,通过关闭终端或计算机切换到睡眠模式,你可能会看到以下错误

ClientImport("Unexpected epoch change")

如果看到此错误,请使用以下命令重新启动节点:

substrate-contracts-node --dev

此命令清除所有正在运行的节点状态。重新启动节点后,重复关闭节点之前执行的任何步骤。例如,重新部署你之前上传的所有合约。

本地存储中的过期合约

Contracts UI 使用它自己的本地存储来跟踪你已经部署的合约。如果你使用 Contracts UI 部署合约,然后清除链上的节点数据,将提示你需要重置本地存储。重置 Contracts UI 的本地存储后,重复清除节点之前执行的所有步骤,并重新部署之前上传的所有合约。