众筹智能合约(下)
原文:https://docs.elrond.com/developers/tutorials/crowdfunding-p2
定义合约参数、处理存储、处理支付、定义新类型、编写更好的测试
配置合约
前一章留给我们一个最小合约作为起点。
我们需要做的第一件事是配置期望的目标金额和截止日期。截止日期将表示为块时间戳,超过该时间后,将不再为合约提供资金。我们将向构造函数添加两个存储字段和参数。
#[view(getTarget)]
#[storage_mapper("target")]
fn target(&self) -> SingleValueMapper<BigUint>;
#[view(getDeadline)]
#[storage_mapper("deadline")]
fn deadline(&self) -> SingleValueMapper<u64>;
#[view(getDeposit)]
#[storage_mapper("deposit")]
fn deposit(&self, donor: &ManagedAddress) -> SingleValueMapper<BigUint>;
#[init]
fn init(&self, target: BigUint, deadline: u64) {
self.target().set(&target);
self.deadline().set(&deadline);
}
作为块时间戳的截止时间可以表示为常规的 64 位无符号整数。然而,作为 EGLD 总和的目标不能。注意 1 埃格勒德= 10^18 埃格勒德-卫(也称为阿托-埃格勒德),最小的货币单位,所有的支付都用卫表示。所以你可以看到,即使是小额支付,数字也会变大。幸运的是,该框架提供了开箱即用的大数支持。有两种类型可用:BigUint 和 BigInt。
尽量避免带符号的版本(除非负值真的有可能并且需要)。BigInt 参数序列化有一些可能导致微妙错误的警告。
还要注意,BigUint 逻辑并不存在于合约中,而是内置于Elrond VM API 中,以避免合约代码膨胀。
让我们测试一下初始化是否有效。
{
"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",
"123,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",
"str:deadline": "123,000"
},
"code": "file:../output/crowdfunding.wasm"
}
}
}
]
}
注意scDeploy
中增加的"arguments"
字段和存储器中增加的字段。
运行以下命令:
erdpy contract build
erdpy contract test
您应该再次看到这一点:
Scenario: crowdfunding-init.scen.json ... ok
Done. Passed: 1\. Failed: 0\. Skipped: 0.
SUCCESS
资助合约
仅仅收到资金是不够的,合约还需要记录谁捐了多少。
#[view(getDeposit)]
#[storage_mapper("deposit")]
fn deposit(&self, donor: &ManagedAddress) -> SingleValueMapper<BigUint>;
#[endpoint]
#[payable("EGLD")]
fn fund(&self) {
let payment = self.call_value().egld_value();
let caller = self.blockchain().get_caller();
self.deposit(&caller).update(|deposit| *deposit += payment);
}
一些需要整理的东西:
- 这个存储映射器有一个额外的地址参数。这就是我们在存储中定义映射的方式。施主参数将成为存储键的一部分。可以添加任意数量的这样的关键参数,但是在这种情况下,我们只需要一个。生成的存储键将是指定的基本键
"deposit"
和序列化参数的串联。 - 我们遇到了第一个可支付函数。默认情况下,智能合约中的任何功能都是不可支付的,即使用该功能向合约发送一笔 EGLD 将导致交易被拒绝。Payable 函数需要用#[payable]标注。
- fund 也需要显式声明为端点。所有的
#[payable]
方法都需要被标记为#[endpoint]
,而不是相反。
为了测试这个功能,我们将在同一个mandos
文件夹中添加一个新的测试文件。姑且称之为crowdfunding-fund.scen.json
。
为了避免重复部署代码,我们从crowdfunding-init.scen.json
导入它。
{
"name": "crowdfunding funding",
"steps": [
{
"step": "externalSteps",
"path": "crowdfunding-init.scen.json"
},
{
"step": "setState",
"accounts": {
"address:donor1": {
"nonce": "0",
"balance": "400,000,000,000"
}
}
},
{
"step": "scCall",
"txId": "fund-1",
"tx": {
"from": "address:donor1",
"to": "sc:crowdfunding",
"egldValue": "250,000,000,000",
"function": "fund",
"arguments": [],
"gasLimit": "100,000,000",
"gasPrice": "0"
},
"expect": {
"out": [],
"status": "",
"gas": "*",
"refund": "*"
}
},
{
"step": "checkState",
"accounts": {
"address:my_address": {
"nonce": "1",
"balance": "1,000,000",
"storage": {}
},
"address:donor1": {
"nonce": "1",
"balance": "150,000,000,000",
"storage": {}
},
"sc:crowdfunding": {
"nonce": "0",
"balance": "250,000,000,000",
"storage": {
"str:target": "500,000,000,000",
"str:deadline": "123,000",
"str:deposit|address:donor1": "250,000,000,000"
},
"code": "file:../output/crowdfunding.wasm"
}
}
}
]
}
解释:
"externalSteps"
允许我们从另一个 json 文件导入步骤。这非常方便,因为我们可以编写从彼此分支的测试场景,而不必复制代码。这里,我们将在所有测试中重用部署步骤。这些导入的步骤在每次导入时都会被再次执行。- 我们需要一个捐献者,所以我们使用新的
"setState"
步骤添加了另一个账户。 - 实际的模拟交易。注意我们用的是
"scCall"
而不是"scDeploy"
。有一个"to"
场,没有"contractCode"
。其余功能相同。"egldValue"
字段表示支付给该功能的金额。 - 在检查状态时,我们有一个新用户,我们看到捐赠人的余额减少了支付的金额,而合约余额增加了相同的金额。
- 合约仓库中还有一个条目。键中的管道符号
|
表示连接。地址本身是序列化的,我们可以用同样可读的格式来表示它。
再次运行命令进行测试:
erdpy contract build
erdpy contract test
然后,您应该看到两个测试都通过了:
Scenario: crowdfunding-fund.scen.json ... ok
Scenario: crowdfunding-init.scen.json ... ok
Done. Passed: 2\. Failed: 0\. Skipped: 0.
SUCCESS
验证
过了截止日期再基金就没有意义了,所以一定的 block 时间戳之后的基金交易必须拒绝。这样做的惯用方法是:
#[endpoint]
#[payable("EGLD")]
fn fund(&self) {
let payment = self.call_value().egld_value();
let current_time = self.blockchain().get_block_timstamp();
require!(current_time < self.deadline().get(), "cannot fund after deadline");
let caller = self.blockchain().get_caller();
self.deposit(&caller).update(|deposit| *deposit += payment);
}
提示
require!(expression, error_msg)
与if !expression { sc_panic!(error_msg) }
相同
sc_panic!("message")
的工作方式类似于标准的panic!
,但是在智能合约环境中工作得更好,效率也更高。常规的panic!
也是允许的,但是它可能会膨胀你的代码,并且你不会看到错误消息。
我们将创建另一个测试文件来验证验证工作:test-fund-too-late.scen.json
。
{
"name": "trying to fund one block too late",
"steps": [
{
"step": "externalSteps",
"path": "crowdfunding-fund.scen.json"
},
{
"step": "setState",
"currentBlockInfo": {
"blockTimestamp": "123,001"
}
},
{
"step": "scCall",
"txId": "fund-too-late",
"tx": {
"from": "address:donor1",
"to": "sc:crowdfunding",
"egldValue": "10,000,000,000",
"function": "fund",
"arguments": [],
"gasLimit": "100,000,000",
"gasPrice": "0"
},
"expect": {
"out": [],
"status": "4",
"message": "str:cannot fund after deadline",
"gas": "*",
"refund": "*"
}
}
]
}
我们这次从crowdfunding-fund.scen.json
开始分支,在那里我们已经有了一个捐赠者。现在,同一个捐献者想要再次捐献,但是与此同时,当前块时间戳变成了 123,001,比最后期限晚了一个块。交易失败,状态为 4(用户错误-合约中的所有错误都将返回此状态)。测试环境还允许我们检查是否返回了正确的消息。
通过再次构建和测试合约,您应该看到所有三个测试都通过了:
Scenario: crowdfunding-fund-too-late.scen.json ... ok
Scenario: crowdfunding-fund.scen.json ... ok
Scenario: crowdfunding-init.scen.json ... ok
Done. Passed: 3\. Failed: 0\. Skipped: 0.
SUCCESS
查询合约状态
任何人都可以通过查看存储和区块链来了解合约状态,但现在真的不方便。让我们创建一个端点来直接给出这个状态。状态将是:FundingPeriod
、Successful
或Failed
中的一种。我们可以在代码中使用一个数字来表示它,但是最好的方法是使用枚举。我们将借此机会展示如何创建一个可序列化的类型,它可以作为参数,作为结果返回或保存在存储中。
这是枚举:
#[derive(TopEncode, TopDecode, TypeAbi, PartialEq, Clone, Copy)]
pub enum Status {
FundingPeriod,
Successful,
Failed,
}
一定要加在合约特质之外。
Rust 中的关键字#[derive]
允许你为你的类型自动实现某些特征。TopEncode
和TopDecode
意味着这种类型的对象是可序列化的,这意味着它们可以被解释为一个字节串。
当您想要与已经部署的合约进行交互时,需要使用TypeAbi
来导出类型。这超出了本教程的范围。
PartialEq
、Clone
和Copy
是 Rust 特征,允许你的类型实例与==
操作符进行比较,Clone
和Copy
特征分别允许你的对象实例被克隆/复制。
我们现在可以像使用其他类型一样使用类型 Status,因此我们可以在合约特征中编写以下方法:
#[view]
fn status(&self) -> Status {
if self.blockchain().get_block_timestamp() <= self.deadline().get() {
Status::FundingPeriod
} else if self.get_current_funds() >= self.target().get() {
Status::Successful
} else {
Status::Failed
}
}
#[view(getCurrentFunds)]
fn get_current_funds(&self) -> BigUint {
self.blockchain().get_sc_balance(&TokenIdentifier::egld(), 0)
}
为了测试这个方法,我们在最后一个测试中增加了一个步骤,test-fund-too-late.scen.json
:
{
"name": "trying to fund one block too late",
"steps": [
{
"step": "externalSteps",
"path": "crowdfunding-fund.scen.json"
},
{
"step": "setState",
"currentBlockInfo": {
"blockTimestamp": "123,001"
}
},
{
"step": "scCall",
"txId": "fund-too-late",
"tx": {
"from": "address:donor1",
"to": "sc:crowdfunding",
"egldValue": "10,000,000,000",
"function": "fund",
"arguments": [],
"gasLimit": "100,000,000",
"gasPrice": "0"
},
"expect": {
"out": [],
"status": "4",
"message": "str:cannot fund after deadline",
"gas": "*",
"refund": "*"
}
},
{
"step": "scQuery",
"txId": "check-status",
"tx": {
"to": "sc:crowdfunding",
"function": "status",
"arguments": []
},
"expect": {
"out": [
"2"
],
"status": "0"
}
}
]
}
因为我们试图调用的函数是一个视图函数,所以我们使用了scQuery
步骤而不是scCall
步骤。不同的是对于scQuery
来说,没有caller
,没有缴费,还有气价/气限。在真实的区块链上,智能合约查询不会在区块链上创建交易,因此不需要帐户。scQuery
模拟了这种行为。
注意最后对“status”的调用和结果"out": [ "2" ]
,这是对Status::Failure
的编码。枚举被编码为其值的索引。在这个例子中,Status::FundingPeriod
是"0"
(或""
),Status::Successful
是"1"
,正如你已经看到的,Status::Failure
是"2"
。
合约函数原则上可以返回任意数量的结果,这就是为什么"out"
是一个列表。
认领功能
最后,我们来补充一下claim
方法。我们刚刚实现的status
方法帮助我们保持代码整洁:
#[endpoint]
fn claim(&self) {
match self.status() {
Status::FundingPeriod => sc_panic!("cannot claim before deadline"),
Status::Successful => {
let caller = self.blockchain().get_caller();
require!(
caller == self.blockchain().get_owner_address(),
"only owner can claim successful funding"
);
let sc_balance = self.get_current_funds();
self.send().direct_egld(&caller, &sc_balance);
},
Status::Failed => {
let caller = self.blockchain().get_caller();
let deposit = self.deposit(&caller).get();
if deposit > 0u32 {
self.deposit(&caller).clear();
self.send().direct_egld(&caller, &deposit);
}
},
}
}
这里唯一的新函数是self.send().direct_egld()
,它只是将 EGLD 从合约转发到给定的地址。
最终合约代码
如果您遵循到目前为止出现的所有步骤,您应该会得到一个类似于以下内容的合约:
#![no_std]
elrond_wasm::imports!();
elrond_wasm::derive_imports!();
#[derive(TopEncode, TopDecode, TypeAbi, PartialEq, Eq, Clone, Copy, Debug)]
pub enum Status {
FundingPeriod,
Successful,
Failed,
}
#[elrond_wasm::contract]
pub trait Crowdfunding {
#[init]
fn init(&self, target: BigUint, deadline: u64) {
require!(target > 0, "Target must be more than 0");
self.target().set(target);
require!(
deadline > self.get_current_time(),
"Deadline can't be in the past"
);
self.deadline().set(deadline);
}
#[endpoint]
#[payable("EGLD")]
fn fund(&self) {
let payment = self.call_value().egld_value();
require!(
self.status() == Status::FundingPeriod,
"cannot fund after deadline"
);
let caller = self.blockchain().get_caller();
self.deposit(&caller).update(|deposit| *deposit += payment);
}
#[view]
fn status(&self) -> Status {
if self.get_current_time() <= self.deadline().get() {
Status::FundingPeriod
} else if self.get_current_funds() >= self.target().get() {
Status::Successful
} else {
Status::Failed
}
}
#[view(getCurrentFunds)]
fn get_current_funds(&self) -> BigUint {
self.blockchain().get_sc_balance(&EgldOrEsdtTokenIdentifier::egld(), 0)
}
#[endpoint]
fn claim(&self) {
match self.status() {
Status::FundingPeriod => sc_panic!("cannot claim before deadline"),
Status::Successful => {
let caller = self.blockchain().get_caller();
require!(
caller == self.blockchain().get_owner_address(),
"only owner can claim successful funding"
);
let sc_balance = self.get_current_funds();
self.send().direct_egld(&caller, &sc_balance);
},
Status::Failed => {
let caller = self.blockchain().get_caller();
let deposit = self.deposit(&caller).get();
if deposit > 0u32 {
self.deposit(&caller).clear();
self.send().direct_egld(&caller, &deposit);
}
},
}
}
// private
fn get_current_time(&self) -> u64 {
self.blockchain().get_block_timestamp()
}
// storage
#[view(getTarget)]
#[storage_mapper("target")]
fn target(&self) -> SingleValueMapper<BigUint>;
#[view(getDeadline)]
#[storage_mapper("deadline")]
fn deadline(&self) -> SingleValueMapper<u64>;
#[view(getDeposit)]
#[storage_mapper("deposit")]
fn deposit(&self, donor: &ManagedAddress) -> SingleValueMapper<BigUint>;
}
作为一个练习,试着增加一些测试,尤其是涉及 claim 函数的测试。
下一步
第一篇 Rust elrond-wasm 教程到此结束。
更多详细文档,请访问https://docs.rs/elrond-wasm/0.35.0/elrond_wasm/index.html
如果你想看一些其他的智能合约示例,甚至是众筹智能合约的扩展版,可以查看这里:https://github . com/elrond network/elrond-wasm-RS/tree/v 0 . 35 . 0/contracts/examples
提示
在 GitHub 上直接进入elrond-wasm
库,首先会看到master
分支。尽管这始终是合约的最新版本,但是它们有时可能依赖于未发布的特性,因此不会在存储库之外编译。然而,从最新发布的版本中获取示例总是安全的。