跳转至

智能合约注解

原文:https://docs.elrond.com/developers/developer-reference/elrond-wasm-annotations

注解(也称为 Rust“属性”)是elrond-wasm智能合约开发框架的基础。虽然原则上可以在没有任何注解或代码生成宏的情况下编写合约,但这样做要困难得多。

该框架的一个主要目的是使代码尽可能的易读和简洁,而注解是达到这一目的的途径。

有关介绍,请查看众筹教程。这个页面应该是智能合约中可能遇到的所有注解的完整索引。

特质注解

T0】

contract注解必须总是放在一个特征上,并将自动使该特征成为智能合约端点和逻辑的主要容器。每箱只能定义一个这样的特征。

请注意,该注解没有附加参数。


T0】

module注解必须总是放在一个特征上,并且将自动使该特征成为一个智能合约模块。

请注意,该注解没有附加参数。

警告

每个 Rust 模块只允许一个合约、模块或代理注解。如果它们在不同的文件中,没有问题,但是如果几个共享一个文件,显式的mod module_name { ... }必须包含模块。


T0】

proxy注解必须总是放在一个特征上,并且将自动使该特征成为一个智能合约调用代理。在合约调用参考中有更多关于智能合约代理的信息。

简而言之,合约总是得到一个自动生成的代理。但是,如果另一个合约的这种自动生成的代理不可用,可以使用proxy属性手工定义这样的“合约接口”。

请注意,该注解没有附加参数。

警告

每个 Rust 模块只允许一个合约、模块或代理注解。如果它们在不同的文件中,没有问题,但是如果几个共享一个文件,显式的mod proxy_name { ... }必须包含模块。

方法注解

T0】

每个智能合约都需要一个构造函数,该构造函数在部署合约时只被调用一次。用 init 注解的方法是构造函数。

#[elrond_wasm::contract]
pub trait Example {
    #[init]
    fn this_is_the_constructor(
        constructor_arg_1: u32,
        constructor_arg_2: BigUint) {
        // ...
    }
} 

升级智能合约时,会调用新代码中的构造函数。它也只被调用一次,而且永远不会被再次调用。

#[endpoint]#[view]

端点是合约的公共方法,可以在交易中调用。一个合约可以定义任意数量的方法,但是只有那些标注了#[endpoint]#[view]的方法对外界可见。

#[view]意在表示只读方法,但目前并没有以任何方式强制这样做。从功能上来说,#[view]#[endpoint]目前完全是同义词。但是,将来有计划强制视图在编译时被验证为只读。当这种情况发生时,已经被正确注解的智能合约将更容易迁移。在此之前,拥有两个注解仍然是有价值的,因为它们表明了意图。

如果没有为属性提供参数,Rust 方法的名称将是端点的名称。或者,可以在括号中提供显式端点名称。

示例:

#[elrond_wasm::contract]
pub trait Example {
    #[endpoint]
    fn example(&self) {
    }

    #[endpoint(camelCaseEndpointName)]
    fn snake_case_method_name(&self, value: BigUint) {
    }

    fn private_method(&self, value: &BigUint) {
    }

    #[view(getData)]
    fn get_data(&self) -> u32{
        0
    } 

在本例中,3 个方法是公共端点。它们被命名为examplecamelCaseEndpointNamegetData。所有其他名称都是内部名称,不会显示在最终的合约中。

所有端点参数和结果必须是可序列化的或特殊的端点参数类型,如MultiValueEncoded。它们还必须都实现了TypeAbi特征。私有方法没有这种限制。

回调

回调有 2 个注解:#[callback]#[callback_raw]。第二种只在极端情况下使用。

回调是特殊的方法,当异步约定调用后出现响应时,会自动调用回调。它们为合约提供了对跨分片调用的结果做出反应的可能性,但是为了一致性,如果异步调用发生在同一个分片中,它们会以相同的方式被调用。

它们还充当闭包,因为它们可以保留最初执行异步调用的交易的一些上下文。

关于它们如何工作的更详细的解释在合约中称为参考文献

储存

开发者可以在合约中手动访问存储,但这很容易出错,并且涉及大量样板代码。出于这个原因,elrond-wasm提供了存储注解,用于在后台管理和序列化键和值。

每个合约都有一个存储区,其中可以存储任意数据。这种存储被组织成任意长度的键和值的映射。区块链没有存储键或值类型的概念,它们都存储为原始字节。解释这些价值是合约的工作。

所有为存储处理而注解的 trait 方法必须没有实现。

T0】

这是从存储中检索数据的最简单方法。让我们从一个用法的例子开始:

#[elrond_wasm::contract]
pub trait Adder {
    #[view(getSum)]
    #[storage_get("sum")]
    fn get_sum(&self) -> BigUint;

    #[storage_get("example_map")]
    fn get_value(&self, key_1: u32, key_2: u32) -> SerializableType; 

首先,请注意,存储方法也可以用#[view]#[endpoint]来注解。端点注解引用合约中方法的角色,而存储注解引用其实现,因此没有重叠。

然后,还要注意有两种方法可以使用这个注解。在第一个例子中,我们简单地在注解中指定了键,从这里开始,这个方法将总是从同一个存储键读取,在这个例子中是"sum"

在第二个例子中,get 方法也接受一些参数。允许任意数量的参数。这些被连接到基本键以形成一个组合键,有效地将合约存储的一部分转换成一个字典或映射。

例如,调用self.get_value(1, 2)将从存储密钥"example_map\x00\x00\x00\x01\x00\x00\x00\x02"0x6578616d706c655f6d61700000000100000002中检索。self.get_value(1, 3)将从存储的不同地方读取,等等。

这是在智能合约中获得哈希表的最简单方法。

最后,存储 getters 必须总是返回一个可反序列化的类型。框架将自动从存储值中找到的任何字节反序列化对象。

T0】

这是将数据写入存储的最简单方式。示例:

#[elrond_wasm::contract]
pub trait Adder {
    #[storage_set("sum")]
    fn set_sum(&self, sum: &BigUint);

    #[storage_set("example_map")]
    fn set_value(&self, key_1: u32, key_2: u32, value: &SerializableType); 

它的工作方式与storage_get非常相似,显著的区别是它不是返回值,而是必须作为参数提供。要存储的值总是最后一个参数。

同样,就像 getter 一样,可以指定任意数量的额外映射键,例如本例中的set_value。这就是我们如何将值写入存储的一个部分,其行为就像一个映射。

警告

没有适当的机制来确保存储键之间没有重叠。没有什么可以阻止开发者编写:

 #[storage_set("sum")]
    fn set_sum(&self, sum: &BigUint);

    #[storage_set("sum")]
    fn set_another_sum(&self, another_sum: &BigUint);

    #[storage_set("s")]
    fn set_value(&self, key: u16, value: &SerializableType); 

第一个问题很容易发现:我们有两个 setters 使用同一个键。

第二个更难注意到。调用self.set_value(0x756d, value)self.set_value(30061, value)也会覆盖"sum"。这是因为"um" = "\x75\6d",串联到"s",形成"sum"

为了避免这个漏洞,永远不要让一个键成为另一个键的前缀!

T0】

存储映射器是可以一次管理多个存储键的对象。他们同时负责读写值。其中一些一次读写多个存储键的值。

框架中有许多存储映射器,还可以自定义更多的映射器。

示例:

 #[storage_mapper("user_status")]
    fn user_status(&self) -> SingleValueMapper<UserStatus>;

    #[storage_mapper("list_mapper")]
    fn list_mapper(&self, sub_key: usize) -> LinkedListMapper<u32>; 

SingleValueMapper是其中最简单的,因为它只管理一个存储键。尽管它只适用于一个存储条目,但它的语法比storage_get / storage_set更简洁,因此被广泛使用。

LinkedListMapper中,我们处理一个条目列表,每个条目都有自己的键。

还要注意,存储映射器也允许附加的子键,与storage_getstorage_set相同。

T0】

这与storage_get非常相似,但是它不是检索值,而是返回一个布尔值,指示序列化值是否为空。它并不试图反序列化值,所以它比storage_get更快更有弹性,这取决于类型。

 #[storage_is_empty("opt_addr")]
    fn is_empty_opt_addr(&self) -> bool; 

如今,使用存储映射器更加普遍。SingleValueMapper有一个is_empty()方法做同样的事情。

T0】

这与storage_set非常相似,但是它不是序列化和写入存储值,而是简单地清除原始字节。它不做任何序列化,所以它可能比storage_set快,这取决于类型。

 #[storage_clear("field_to_clear")]
    fn clear_storage_value(&self); 

如今,使用存储映射器更加普遍。SingleValueMapper有一个clear()方法做同样的事情。

事件

事件是从 smart contract 返回数据的一种方式,通过留下执行期间发生的事情的痕迹。事件日志不会保存在区块链上,但会保存它们的哈希。这意味着我们可以随时检查某个交易是否发出了某些事件。

因为不是全额保存在链上,所以也比存储便宜很多。

在智能合约中,我们将它们定义为没有实现的特征方法,如下所示:

 #[event("transfer")]
    fn transfer_event(
        &self,
        #[indexed] from: &ManagedAddress,
        #[indexed] to: &ManagedAddress,
        #[indexed] token_id: u32,
        data: ManagedBuffer,
    ); 

注解总是要求在括号中明确指定事件的名称。

事件有两种类型的参数:

  • “主题”用#[indexed]标注。将事件日志保存到数据库时,将为所有这些字段创建索引,以便可以有效地搜索它们。
  • “数据”参数没有注解。一个事件中只能有一个数据字段,并且以后不能被索引。

事件参数(字段)可以是任何可序列化的类型。事件没有返回值。

事件(遗留)

有一个遗留注解,#[legacy_event]仍被一些较老的合约使用。它已被弃用,不应再使用。

T0】

这是一个简单的 getter,它提供了一个方便的合约代理实例。当想要调用另一个合约时使用它。

#[elrond_wasm::module]
pub trait ForwarderAsyncCallModule {
    #[proxy]
    fn vault_proxy(&self, to: Address) -> vault::Proxy<Self::Api>;

    // ...
} 

不需要参数,注解将通过提供的返回类型计算出要调用的合约。

重要

代理类型需要用显式模块指定。在示例中,vault::是强制的。

T0】

这是用于 ABI 结果名称。在 Rust 中,不可能为方法返回编写 Rust 文档,所以我们使用这个注解来随意命名一个端点的输出。



回到顶部