众筹智能合约(上)
原文:https://docs.elrond.com/developers/tutorials/crowdfunding-p1
用 Rust 编写、构建和部署一个简单的智能合约
本教程将指导您为Elrond网络编写、构建和部署一个非常简单的智能合约,该合约是用 Rust 编写的。
重要
Elrond网络支持用任何编程语言编写的智能合约,但是它们必须被编译成 WebAssembly。
重要
当前教程围绕着 elrond-wasm-rs 版本 0.35.0 展开,并将随着 elrond-wasm 新版本的发布而更新。
简介
比方说,你需要为你相信的事业筹集 EGLD。它们显然会被很好的利用,但是你需要先得到 EGLD。出于这个原因,你决定在Elrond网络上开展一项众筹活动,这自然意味着你将在活动中使用智能合约。本教程将教你如何做到这一点:写一份众筹智能合约,如何部署和如何使用它。
想法很简单:智能合约将接受转账,直到截止日期,它将跟踪所有发送 EGLD 的人。
如果截止日期到了,智能合约已经收集了超过所需资金的 EGLD 数量,那么智能合约将认为众筹成功,并因此将所有 EGLD 发送到预定的帐户(您!).
但是如果 EGLD 的总量低于预期目标,则必须将所有捐献的 EGLD 送回捐献者手中。
设计
智能合约是这样设计的:
- 它将有一个
init
方法,在部署时自动执行。该方法必须从您处获得以下信息:(EGLD 的目标金额和(2)众筹截止日期,以整批随机数表示。 - 它将有一个
fund
方法,人们将调用该方法向智能合约发送资金。这个方法将接收 EGLD,并且必须保存所有需要的信息,以便在活动没有达到目标时返回 EGLD。 - 它将有一个
claim
方法。如果有人在截止日期之前调用这个方法,它将什么都不做并返回一个错误。但是如果在截止日期之后调用,它将执行以下操作之一:- 当你调用它时,目标金额已经达到,它将把所有的 EGLD 发送给你。如果没有达到这个数量,它将什么也不做,并返回一个错误。
- 当其中一个捐赠者调用它,并且已经达到目标金额时,它将什么也不做并返回一个错误。但是如果还没有达到这个数量(活动失败),那么智能合约会将正确数量的 EGLD 发送回给捐赠者。
- 当其他任何人调用它时,该方法将不做任何事情,并将返回一个错误。
- 它将有一个
status
方法,该方法将返回关于活动的信息,例如活动是正在进行还是已经结束,以及到目前为止已经捐赠了多少 EGLD。出于不耐烦,您可能会经常调用这个方法。
四种方法,然后:init
、fund
、claim
和status
。
本教程将首先关注init
方法,让你熟悉开发过程和工具。你将实现init
并且为它编写单元测试。
检测
自动化测试对于智能合约的开发非常重要,因为它们必须处理敏感的信息。
先决条件
在Elrond上构建的最好方式是使用我们的 VS Code IDE ,您应该在继续之前安装它。
Elrond IDE 是 Visual Studio 代码的扩展,为Elrond智能合约提供开发支持。
Elrond IDE 支持以下编程语言:
- 铁锈色-推荐。对于 Rust,IDE 还通过 elrond-wasm-debug 和 CodeLLDB 提供了一步一步的调试体验。
- C / C++
跟随视频指南,了解如何开始的详细说明。
https://www.youtube-nocookie.com/embed/bXbBfJCRVqE?playlist=bXbBfJCRVqE&loop=1
下面详细介绍了这些步骤。
第一步:工作区
每个智能合约的源代码都需要自己的文件夹。您需要为这里介绍的众筹智能合约创建一个。在终端中运行以下命令来创建它:
mkdir -p ~/Elrond/SmartContracts
cd ~/Elrond/SmartContracts
erdpy contract new crowdfunding --template adder
code crowdfunding
您可以为您的智能合约选择任何您想要的位置。以上只是一个例子。无论哪种方式,现在您都位于智能合约专用的文件夹中,我们可以开始了。
你马上就会得到一个可以工作的项目- erdpy
用一个模板创建你的项目。这些模板是由Elrond编写和测试的合约,任何人都可以将其作为起点。adder
模板是你能想象到的最简单的合约。
最后一行还在新的 VS 代码实例中打开新项目。
让我们快速浏览一下这个项目。
在您选择的文本编辑器中打开Cargo.toml
,添加以下内容:
[package]
name = "crowdfunding"
version = "0.0.1"
authors = [ "you",]
edition = "2018"
[lib]
path = "src/crowdfunding_main.rs"
[dependencies.elrond-wasm]
version = "0.35.0"
[dev-dependencies.elrond-wasm-debug]
version = "0.35.0"
让我们看看这意味着什么:
- 不出所料,这个包被命名为
crowdfunding
,版本为0.0.1
。你可以设置任何你喜欢的版本,只要确保它有 3 个用点分隔的数字。这是要求。 - 此包有依赖项。它将需要其他包。因为你正在为Elrond网络写一份 Rust smart 合约,你将需要 3 个由Elrond开发的特殊且非常有用的软件包。
- 文件
src/crowdfunding_main.rs
将包含智能合约的源代码,这就是[lib]
部分声明的内容。您可以随意命名该文件。默认的 Rust 命名是lib.rs
,但是当主代码文件带有合约名称时,组织代码会更容易。 - 基于板条箱名称,生成的二进制文件将被命名为
crowdfunding
(实际上是crowdfunding.wasm
,但是编译器将添加.wasm
部分)。
第二步:代码
有了合适的结构,现在就可以编写代码并构建它了。打开src/lib.rs
,删除现有的Adder
代码并插入以下内容:
#![no_std]
elrond_wasm::imports!();
#[elrond_wasm::contract]
pub trait Crowdfunding {
#[init]
fn init(&self) {
}
}
让我们看一下代码。前三行声明了代码的一些特征。你不需要理解它们(如果你愿意,可以直接跳过),但这里有一些解释:
no_std
意味着智能合约不能访问标准库。这听起来可能有限制性,但是代价是代码将会精简并且非常轻。使用标准库创建智能合约是完全可能的,但是这会增加很多开销,不推荐这样做。众筹智能合约肯定不需要。
带入框架
第三行包含命令elrond_wasm::imports!();
。这个命令导入我们在讨论Cargo.toml
文件时提到的依赖项。它有效地允许您访问 Rust 智能合约的Elrond框架,该框架旨在极大地简化代码。
框架本身是另一天的主题,但是您应该知道用 Rust 编写的智能合约通常没有这么简单。是框架完成了繁重的工作,所以你的代码保持干净和可读。第 5 行是您与框架的第一次接触:
#[elrond_wasm::contract]
这一行只是告诉框架将下一个trait
声明(我们稍后会谈到)视为智能合约。因为这一行,框架将自动生成所需的大部分代码。您现在看不到生成的代码(但您可以看到)。
让它成为一种特质
您的智能合约实际上从第 9 行开始。我们本来可以更快到达这里,但你想知道代码的含义,这需要一点时间来解释。不过,我们终于来了。让我们再看一下代码:
在继续之前,知道 Rust 中的特征是什么是有帮助的(Rust 的书很好地解释了这个问题)。
现在,您只需要记住,您将您的智能合约编写为trait Crowdfunding
,以便允许Elrond框架为您生成支持代码,从而产生一个隐藏的struct CrowdfundingImpl
。
Init
每个智能合约都必须定义一个构造器方法,在部署到网络上时,该方法只运行一次。你可以随意命名,但必须用#[init]
标注。众筹智能合约需要存储一些初始配置,在后续调用其他方法(这些其他方法是fund
、claim
和status
,刷新一下你的记忆)时会读取这些配置。
众筹智能合约的init
方法目前是空的。我们稍后将添加实际的代码。首先,您想要构建整个项目,以确保到目前为止一切都运行良好,即使智能合约现在什么都不做。
第三步:构建
在用前一步中的描述的内容创建文件src/crowdfunding_main.rs
之后,您可以发出第一个构建命令。请确保首先保存文件。
现在回到终端,确保当前文件夹是包含众筹智能合约的文件夹(使用pwd
),然后发出构建命令:
erdpy contract build
如果这是您第一次使用erdpy
命令构建 Rust smart 合约,那么在它完成之前需要一点时间。后续的构建会快得多。
命令完成后,会出现一个新的文件夹:output
。这个文件夹现在包含两个文件:crowdfunding.abi.json
和crowdfunding.wasm
。我们还不会对这些文件做任何事情——等到部署部分。随着output
,还有一些其他的文件夹和文件生成。你现在可以安全地忽略它们,但是不要删除wasm
文件夹——它使得构建命令在初次运行后更快。
以下内容可以安全删除,因为它们对本合约不重要:
snippets.sh
和elrond.json
文件tests
文件夹interaction
文件夹
你的文件夹的结构应该是这样的(由命令tree -L 3
输出):
.
├── Cargo.toml
├── mandos
│ └── crowdfunding.scen.json
├── meta
│ ├── Cargo.toml
│ └── src
│ └── main.rs
├── output
│ ├── crowdfunding.abi.json
│ └── crowdfunding.wasm
├── src
│ └── crowdfunding_main.rs
└── wasm
├── Cargo.toml
└── src
└── lib.rs
现在是时候给init
函数添加一些功能了,因为下一步将带您经历一个非常重要的过程:测试您的智能合约。
第四步:测试
在这一步中,您将使用init
方法在众筹智能合约的存储中保存一些值。之后,我们将编写一个测试来确保这些值被正确存储。
存储映射器
每个智能合约都可以将键值对存储到一个持久结构中,该持久结构是在智能合约部署到Elrond网络时为智能合约创建的。
智能合约的存储实际上是一个通用的哈希映射或字典。当你想存储一些任意值时,你把它存储在一个特定的键下。要取回值,您需要知道存储它的键。
为了帮助您保持代码的整洁,框架允许您为单独的键值对编写 setter 和 getter 方法。有几种方法可以通过合约与存储进行交互,但最简单的方法是使用存储映射器。下面是一个简单的映射器,专门用于存储/检索键target
下存储的值:
#[storage_mapper("target")]
fn target(&self) -> SingleValueMapper<BigUint>;
上述方法将存储值视为具有特定的类型,即类型BigUint
。在幕后,BigUint
是一个大的无符号数,由 VM 处理。没有必要导入任何库,大数字算法是为所有合约提供了开箱即用。
通常,智能合约开发者习惯于在存储或从存储中加载值时处理原始字节。Rust smart contracts 的Elrond框架使管理存储变得容易得多,因为它可以自动处理键入的值。
设定一些目标
现在,您将指示init
方法在部署时存储应该收集的代币数量。
智能合约的所有者是部署它的客户(您)。根据设计,您的众筹智能合约将把所有捐赠的 EGLD 发送给其所有者(您),假设达到了目标金额。其他人都没有这个特权,因为任何给定的智能合约都只有一个所有者。
下面是init
方法的样子,代码保存了目标(猜猜是谁):
#![no_std]
elrond_wasm::imports!();
#[elrond_wasm::contract]
pub trait Crowdfunding {
#[storage_mapper("target")]
fn target(&self) -> SingleValueMapper<BigUint>;
#[init]
fn init(&self, target: BigUint) {
self.target().set(&target);
}
}
我们向构造函数方法添加了一个参数,这个参数叫做target
,在我们部署合约时需要提供。然后,参数被适当地保存到存储器中。
现在请注意self.target()
调用。这为我们提供了一个对象,该对象充当存储的一部分的代理。对其调用.set()
方法会将值保存到合约存储中。
不完全是。只有当交易成功完成时,所有存储的值才真正在存储器中结束。智能合约不能直接访问协议,它是所有事情的中介。
每当您希望确保代码有序时,运行 build 命令:
erdpy contract build
还有一点:默认情况下,fn
语句中没有一个声明智能合约方法是外部可调用的。合约中的所有数据都是公开可用的,但是手动搜索合约存储可能很麻烦。这就是为什么公开 getters 通常是好的,这样人们可以调用它们来获取特定的数据。公共方法用#[endpoint]
或#[view]
进行注释。目前它们在功能上没有区别(但将来某个时候可能会有区别)。从语义上来说,#[view]
表示只读方法,而#[endpoint]
表示该方法也改变合约状态。您也可以将#[init]
视为一种特殊类型的端点。
#[view]
#[storage_mapper("target")]
fn target(&self) -> SingleValueMapper<BigUint>;
但是你会记得吗?
您必须始终确保您编写的代码按预期运行。这就是自动测试的目的。
让我们针对init
方法编写一个测试,确保它在部署时明确地将所有者的地址存储在target
键下。
为了测试init
,您将编写一个 JSON 文件,描述如何处理智能合约以及预期的输出是什么。在众筹智能合约的文件夹里,有一个文件夹叫mandos
。在它里面,有一个叫做crowdfunding.scen.json
的文件。将该文件重命名为crowdfunding-init.scen.json
( scen
是“场景”的简称)。
你的文件夹应该是这样的(命令tree -L 3
的输出):
.
├── Cargo.toml
├── debug
│ ├── Cargo.toml
│ └── src
├── mandos
│ └── crowdfunding-init.scen.json
├── meta
│ ├── Cargo.toml
│ └── src
│ └── main.rs
├── output
│ ├── crowdfunding.abi.json
│ └── crowdfunding.wasm
├── src
│ └── lib.rs
└── wasm
├── Cargo.lock
├── Cargo.toml
├── src
└── target
让我们定义第一个测试场景。在您喜欢的文本编辑器中打开文件mandos/crowdfunding-init.scen.json
,并用以下代码替换其内容。这可能看起来很多,但是我们会检查它的每一点,它并不真的那么复杂。
{
"name": "crowdfunding deployment test",
"steps": [
{
"step": "setState",
"accounts": {
"address:my_address": {
"nonce": "0",
"balance": "1,000,000"
}
},
"newAddresses": [
{
"creatorAddress": "address:my_address",
"creatorNonce": "0",
"newAddress": "sc:crowdfunding"
}
]
},
{
"step": "scDeploy",
"txId": "deploy",
"tx": {
"from": "address:my_address",
"contractCode": "file:../output/crowdfunding.wasm",
"arguments": [
"500,000,000,000"
],
"gasLimit": "5,000,000",
"gasPrice": "0"
},
"expect": {
"out": [],
"status": "0",
"gas": "*",
"refund": "*"
}
},
{
"step": "checkState",
"accounts": {
"address:my_address": {
"nonce": "1",
"balance": "1,000,000",
"storage": {}
},
"sc:crowdfunding": {
"nonce": "0",
"balance": "0",
"storage": {
"str:target": "500,000,000,000"
},
"code": "file:../output/crowdfunding.wasm"
}
}
}
]
}
保存文件。你想先试试吗?继续在您的终端上发出以下命令:
erdpy contract test
如果一切顺利,您应该会看到一个大写的、响亮的SUCCESS
正在打印,就像这样:
Scenario: crowdfunding-init.scen.json ... ok
Done. Passed: 1\. Failed: 0\. Skipped: 0.
SUCCESS
您需要理解这个 JSON 文件的内容——同样,测试您的智能合约的重要性不能被夸大。
那么刚才发生了什么呢?
您运行了一个解释 JSON 场景的测试命令。第 2 行包含该场景的名称,即crowdfunding deployment test
。该测试是在一个隔离的环境中执行的,该环境包含Elrond WASM 虚拟机和模拟的区块链。这是你能得到的最接近真实的Elrond网络的东西——当然,除了运行你自己的本地测试网,但是你现在不需要考虑这个。
一个场景有多个步骤,这些步骤将按照它们在 JSON 文件中出现的顺序执行。注意第 3 行的字段steps
是一个 JSON 列表,包含三个场景步骤。
查看 JSON 文件,您可能会认为"step": "setState"
的意思仅仅是给场景步骤命名。这是不正确的,因为"step": "setState"
意味着这一步的类型是setState
,也就是为接下来的场景步骤准备测试环境的状态。
对于"step": "scDeploy"
也是如此,这是一个执行 SmartContract 部署的场景步骤。正如您可能猜到的,最后一个场景步骤的类型是checkState
:它描述了在运行前面的场景步骤之后,您对测试环境的期望。
以下小节将分别讨论每个步骤。
场景步骤【setState】
你就是你,但在不同的宇宙里
第一个场景步骤从声明虚拟世界中存在的帐户开始,众筹智能合约将在该虚拟世界中进行测试。
只定义了一个帐户——将在测试期间执行部署的帐户。智能合约将认为它属于该帐户。在 JSON 文件中,您写道:
"accounts": {
"address:my_address": {
"nonce": "0",
"balance": "1,000,000"
}
},
这用地址my_address
定义了帐户,测试环境将使用它来假装是您。请注意,在这个虚构的世界中,您的帐户 nonce 是0
(意味着您从未使用过这个帐户),您的balance
是1,000,000
。注意:EGLD 有 18 位小数,所以 1 EGLD 将等于1,000,000,000,000,000,000
(10^18),但是你很少需要在测试中使用这么大的值。
注意在my_address
的开头有文本address:
,它指示测试环境将紧随其后的字符串视为 32 字节地址(通过添加必要的填充以达到所需的长度),也就是说,它不应该试图将其解码为十六进制数或其他任何东西。上面 JSON 文件中的所有地址都用前导address:
定义,所有智能合约都用sc:
定义。
虚数地址生成器
紧随accounts
之后,第一个场景步骤包含以下块:
"newAddresses": [
{
"creatorAddress": "address:my_address",
"creatorNonce": "0",
"newAddress": "sc:crowdfunding"
}
]
简而言之,这个块指示测试环境假装为my_address
尝试的第一次(nonce 0
)部署生成的地址必须是地址crowdfunding
。
有道理,不是吗?如果你没有写这个,测试环境会在一些自动生成的地址部署众筹智能合约,我们不会被告知,所以我们不能在后续的场景步骤中与智能合约进行交互。
但是使用配置好的newAddresses
生成器,我们知道每次运行测试都会在地址the_crowdfunding_contract
部署智能合约。
虽然现在知道这些并不重要,但是可以将newAddresses
生成器配置为为多个智能合约部署甚至为执行部署的多个地址生成固定地址!
场景步骤【sc deploy】
JSON 文件定义的下一个场景步骤指示测试环境自己执行部署。观察:
"tx": {
"from": "address:my_address",
"contractCode": "file:../output/crowdfunding.wasm",
"arguments": [ "500,000,000,000" ],
"value": "0",
"gasLimit": "1,000,000",
"gasPrice": "0"
},
这描述了一个部署交易。它是由“您”使用您的地址为my_address
的帐户虚构提交的。
这个部署交易包含众筹智能合约的 WASM 字节码,在运行时从文件output/crowdfunding.wasm
中读取。
记得在运行测试之前运行erdpy contract build
,尤其是如果您最近对智能合约源代码进行了更改!WASM 字节码将直接从您在这里指定的文件中读取,不需要自动重新构建。
“您”还将1,000,000
中的value: 0
EGLD 发送到已部署的智能合约。反正它也不需要它们,因为你的众筹智能合约不会把任何 EGLD 转让给任何人,除非他们先捐了。
字段gasLimit
和gasPrice
不应该让你太担心。重要的是gasLimit
需要高,gasPrice
可能是 0。如你所知,真正的Elrond网络会从这些值中计算交易费用。在真实的Elrond网络中,由于显而易见的原因,您不能将gasPrice
设置为 0。
部署的结果
一旦测试环境执行了上面描述的部署交易,您就有机会断言它成功完成了:
"expect": {
"out": [],
"status": "0",
"gas": "*",
"refund": "*"
}
这里唯一重要的字段是"status": "0"
,它是执行部署交易后来自Elrond虚拟机的实际返回代码。0
当然是成功的意思。
out
数组将包含由您的智能合约调用返回的值(在这种情况下,init
函数不返回任何内容,但是如果开发者愿意,它可以返回)。
剩下的两个字段gas
和refund
允许您指定您期望部署交易消耗多少 gas,以及由于高估了gasLimit
您将收到多少 EGLD。这里它们都被设置为"*"
,这意味着我们现在不关心它们的实际值。
场景步骤【检查状态】
最后一个场景步骤与第一个场景步骤相同。还有一个accounts
字段,但内容更多:
"accounts": {
"address:my_address": {
"nonce": "1",
"balance": "1,000,000"
},
"sc:crowdfunding": {
"code": "file:../output/crowdfunding.wasm",
"nonce": "0",
"balance": "0",
"storage": {
"str:target": "500,000,000,000"
}
}
}
请注意,现在有两个帐户,而不是一个。显然有一个账户my_address
,在第一个场景步骤中我们自己定义了它之后,我们知道它存在。但是作为第二个场景步骤中执行的部署交易的结果,出现了一个新帐户the_crowdfunding_contract
。这是因为智能合约是Elrond网络中的账户,这些账户具有相关的代码,当交易发送给它们时,这些代码可以被执行。
账户my_address
现在有了随机数1
,因为已经执行了一个交易,并从其发送。它的余额保持不变——部署交易没有花费任何成本,因为在第二个场景步骤中,gasPrice
字段被设置为0
。当然,这只允许在测试中使用。
账户crowdfunding
就是众筹智能合约。我们断言它包含由文件output/crowdfunding.wasm
(相对于 JSON 文件的路径)指定的字节码。我们还断言它的nonce
是0
,这意味着合约本身从未部署过自己的“子”合约(这在技术上是可能的)。智能合约帐户的balance
是0
,因为它没有接收任何 EGLD 作为部署交易的一部分,我们也没有指定任何场景步骤将 EGLD 转账给它(我们很快就会这么做)。
最后,我们断言智能合约存储在target
键下包含500,000,000,000
,这是init
函数应该确保的。因此,智能合约记住了你为它设定的目标。
接下来是
本教程将继续介绍fund
、claim
和status
函数的定义,并指导您为它们编写 JSON 测试场景。