跳转至

众筹智能合约(下)

原文: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);
    } 

一些需要整理的东西:

  1. 这个存储映射器有一个额外的地址参数。这就是我们在存储中定义映射的方式。施主参数将成为存储键的一部分。可以添加任意数量的这样的关键参数,但是在这种情况下,我们只需要一个。生成的存储键将是指定的基本键"deposit"和序列化参数的串联。
  2. 我们遇到了第一个可支付函数。默认情况下,智能合约中的任何功能都是不可支付的,即使用该功能向合约发送一笔 EGLD 将导致交易被拒绝。Payable 函数需要用#[payable]标注。
  3. 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"
                }
            }
        }
    ]
} 

解释:

  1. "externalSteps"允许我们从另一个 json 文件导入步骤。这非常方便,因为我们可以编写从彼此分支的测试场景,而不必复制代码。这里,我们将在所有测试中重用部署步骤。这些导入的步骤在每次导入时都会被再次执行。
  2. 我们需要一个捐献者,所以我们使用新的"setState"步骤添加了另一个账户。
  3. 实际的模拟交易。注意我们用的是"scCall"而不是"scDeploy"。有一个"to"场,没有"contractCode"。其余功能相同。"egldValue"字段表示支付给该功能的金额。
  4. 在检查状态时,我们有一个新用户,我们看到捐赠人的余额减少了支付的金额,而合约余额增加了相同的金额。
  5. 合约仓库中还有一个条目。键中的管道符号|表示连接。地址本身是序列化的,我们可以用同样可读的格式来表示它。

再次运行命令进行测试:

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 

查询合约状态

任何人都可以通过查看存储和区块链来了解合约状态,但现在真的不方便。让我们创建一个端点来直接给出这个状态。状态将是:FundingPeriodSuccessfulFailed中的一种。我们可以在代码中使用一个数字来表示它,但是最好的方法是使用枚举。我们将借此机会展示如何创建一个可序列化的类型,它可以作为参数,作为结果返回或保存在存储中。

这是枚举:

#[derive(TopEncode, TopDecode, TypeAbi, PartialEq, Clone, Copy)]
pub enum Status {
    FundingPeriod,
    Successful,
    Failed,
} 

一定要加在合约特质之外。

Rust 中的关键字#[derive]允许你为你的类型自动实现某些特征。TopEncodeTopDecode意味着这种类型的对象是可序列化的,这意味着它们可以被解释为一个字节串。

当您想要与已经部署的合约进行交互时,需要使用TypeAbi来导出类型。这超出了本教程的范围。

PartialEqCloneCopy是 Rust 特征,允许你的类型实例与==操作符进行比较,CloneCopy特征分别允许你的对象实例被克隆/复制。

我们现在可以像使用其他类型一样使用类型 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分支。尽管这始终是合约的最新版本,但是它们有时可能依赖于未发布的特性,因此不会在存储库之外编译。然而,从最新发布的版本中获取示例总是安全的。



回到顶部