Ink是一种智能合约语言,专门用于基于Substrate的区块链。我们将访问minting、transfer和approval函数的实现,并解释在开发中遇到的语法。Ink建立在Rust编程语言之上,因此我们将在这里探讨一些Rust概念以及它们如何与我们的智能合约一起工作。
在这篇文章继续之前,建议阅读一下上一篇文章,我们介绍了Ink,如何将它与Rust和Substrate一起安装,以及Ink语法如何在高级别上与Solidity不同。
上一篇文章对Ink智能合约的结构进行了分解;在这里我们将更详细地访问该实现。回顾如下,结构如下:
模块声明
Ink不依赖于Rust标准库 - 而是导入Ink模块来编写所有合同逻辑。 让我们快速浏览一下我们导入智能合约的内容:
我们在这里公开智能合约中需要使用哪些模块,导入storage 和memory的ink_core重要模块,以及一些env对象,关键数据,例如地址的调用者。 此外,已经从parity_codec声明了Encode和Decode,用于将事件编码为原始格式。
在模块声明之前,您还会注意到以下内容:
如果我们运行测试模块,或者如果我们在代码中使用std功能标志,则该行声明我们正在使用标准库。 否则合同将始终使用no_std进行编译。 Ink合约不使用Rust标准库,因此除非我们明确定义它,否则它将从编译中省略。
事件定义
Event,也可以看作是区块链通知也是智能合约的一个重要方面;它们在发生事情时主动地发出数据,允许DAPP以实时方式对其作出反应。因此,我们的NFTOKEN合同定义了3个事件:minting、transfer和Approval。
它们是在我们的contract! 之前定义的宏。mint事件期望在调用或发出事件时提供accountID和u64值:
accountID类型由ink core提供;如果您回忆上一节,我们通过ink_core中的env模块的破坏语法导入了这两种类型。accountID代表一个帐户(相当于以太坊的地址类型)。另一个可用的类型是balance,它是u64类型,或者64位无符号整数。
注意:我们可以使用Balance类型代替u64来表示标记值。 尽管Balance类型最好与令牌值一起使用,但我在使用类型时遇到了一些歧义,其中编译的合同不喜欢在Balance值中添加u64值。 可以想象,随着Ink的进一步发展,Balance将在未来得到增强,提供更多属性,进一步代表Balance,例如单位类型。 一旦围绕其使用的模糊性被清除,将在NFToken合同中实施余额。
我们还在事件定义下面定义了一个private deposit_event函数:
这只是一个包含Inks提供的deposit_raw_event函数的事件函数,期望将编码事件作为其唯一参数。
注意:env :: deposit_raw_event函数调用后没有分号? 在Rust中,从函数的最后一个表达式中省略分号会返回该表达式的结果,从而无需编写return,但如果您想要进一步返回该函数,则完全有效。
关于Rust的所有权机制的说明
要理解的另一个重要的Rust(因此Ink)编程概念是所有权。 我们的deposit_event功能使用所有权。 在env :: deposit_raw_event中查看Event参数之前的&used:
在rust中,&表示对对象的引用。
如果我们没有使用引用,env :: deposit_raw_event将获取事件的所有权,因此将不再可用于deposit_event()。 event将“move”到包含函数中,并且不再在外部函数的范围内。 如果我们在此之后尝试使用event,则会收到错误,因为该范围中不再存在event。
即使我们的deposit_event()函数只有一行代码,因此将event移出作用域不会对函数的其余部分产生影响,env::deposit_raw_event实际上需要一个引用。查看删除引用时收到的错误:
在处理Rust所有权时,编辑器非常有用,并且会在尝试编译程序之前确保解决所有权问题。 在这种情况下,它实际上告诉我们如何在帮助部分下修复此错误。
要阅读有关Rust所有权的更多信息,The Rust Book有一个很棒的部分解释了这些概念; 在努力进入Ink智能合约编程之前,建议了解Rust的所有权。
通过定义我们的事件(以及用于发出这些事件的辅助函数),现在让我们探讨 contract!的内容 宏本身。
智能合约变量
智能合约变量可以被认为是通过self在函数内访问的类属性。 以下是我们的NFToken合同的合约变量:
前两个变量的类型为storage :: Value,以及以下三个storage :: HashMap。 事实上,ink_core storage模块必须用于我们希望在链上持久存在的任何合同数据。
storage类型是通用的,因此我们在尖括号中明确提供了我们存储的数据类型。
通过定义所需的合同数据,让我们探讨一些合同实现,突出显示一些关键逻辑和语法。
智能合约部署
部署函数在任何Ink智能合约中都是强制的,并且在部署到一个链上时实例化一个智能合约时被调用。
将deploy()函数包含在impl Deploy for
我们只是在这里设置默认值,添加了一些初始令牌铸币。 我们接下来将探讨minting实施。
minting实施
Minting是生成新令牌的过程。 对于我们的NFToken合同,Minting需要满足以下条件:
每个标记必须具有由token_id表示的唯一索引
需要提供AccountId来填充令牌
只有合同所有者才能铸造新的代币
声明公共函数mint()来处理对mint标记的调用:
声明公共函数mint()来处理对mint标记的调用:
Mint接受两个论点:minting账户和数量,函数签名的第一个参数始终是对self的引用。 此外,我们还可以包含mut来声明self可以更新,实质上是为合同实例提供了一个可变引用。
mint()调用私有mint impl()函数,该函数执行实际的minting过程。这种通过公共函数公开私有函数的模式对于传输和批准也是一致的。
mint_impl()将执行以下任务:
设计出第一个新的TokKyId和最后一个TKEN ID。这是根据self.total创建的合同变量计算的。
我们定义一个for循环,它将增加标记id并将每个标记id插入self.id_to_owner哈希映射。此循环的特定语法很有趣,采用了for-in结构,并采用了spread运算符:
Ink的HashMap实现与标准的Rust实现非常相似。 insert()将向我们的映射添加一条新记录。 查看我们可以操作HashMap的所有方法的完整参考。
关于解除引用的说明,附*
要获得合同变量的原始值,我们需要“取消引用”它们。 解除引用的概念在The Rust Book中有详细解释,但基本上解除引用允许我们获取指针或引用的基础值。
让我们看看我们如何计算mint_impl()中的start_id作为使用解除引用的示例:
将鼠标悬停在self.total上会显示我们需要取消对storage::value的引用以获取基础的U64值。与引用类似,编辑器非常智能,当表达式没有意义时(例如,尝试向storage::value对象添加1,这将导致错误),编辑器就会意识到这一点。
尽管可能不建议将取消引用作为修复方法,但一旦在编辑器中指出错误,程序员就应该知道这一点。
一旦新的令牌被分配给id_to_owner,所有者_to_token_count映射也会更新,反映出所有者拥有的新令牌数量。此外,total-minted也会更新以反映新铸造的代币。
您可能已经注意到我们更新owner_to_token_count哈希映射的方式在首次检查时可能会有些混乱。 以下是执行此操作的代码行:
第一行代码尝试使用get()从哈希映射中检索现有令牌计数,或者如果找不到,则将0的值赋给from_owner_count。下一行应该很熟悉,我们使用insert()插入新记录或覆盖现有记录。
但unwrap_or()实际上做了什么? 好吧,get()实际上返回一个Option枚举,而不是返回令牌计数本身,然后我们可以在值存在的情况下解包。 如果某个值不存在,我们可以提供一个替代值,作为unwrap_or()的参数,在上述情况下为0。
让我们进一步简要探讨这个概念; 它不仅用于合同的其他领域,它也是Rust的基本设计模式。
了解Rust选项枚举
正如我们已经确定的那样,从HashMap契约变量中获取值实际上会产生Option枚举。 Rust中的Option枚举提供了两个可能的值:Some或None。 这基本上避免了空值,在值不存在的情况下返回None。
现在,我们的NFToken智能合约中使用的一个常见约定是首先检查HashMap中是否存在值,并在返回None选项的某些情况下返回false。 在存在Some值的情况下,我们然后在Option值上使用unwrap修饰符来获取Some包装的值。
is_token_owner()函数是遵循此模式的一个示例:
与使用unwrap_或()(如前一个示例中)不同,unwrap()只是假设所有者是Some选项; 我们已经处理了owner是一个none值的情况,因此可以安全地假设存在一个要unwrap的值。
为了结束is_token_owner(),我们检查检索到的令牌所有者是否与我们在函数调用中提供的AccountId匹配:
我们现在已经涵盖了mint_impl()的大部分内容。函数的最后一行发出一个事件,我们将其定义为一个私有函数:
我们的私有事件发出函数通过断言简单地检查传入的值是否有效,然后调用早先声明的SaveItIGIVER事件()函数。
我们的铸造实施已经向我们介绍了在合同实施的其余部分中使用的概念和约定。让我们访问传递函数文本。
转移实施
转让代币可以说是我们合同中最重要的特征,允许我们将代币转移到账户和从账户转移代币。 public transfer()函数可用于发送传输请求,并具有以下签名:
此函数调用private transfer_impl()函数来执行实际的传输逻辑。确保满足以下条件进行转移:
我们立即检查调用者是否是令牌所有者,如果不是,则返回false
更新id_to_token映射,将token_id索引的值覆盖到新所有者的帐户
更新令牌计数,减少发件人的计数并增加接收者的计数
Transfer事件通过emit_transfer()发出
可以说,这是一个比生成过程更简单的函数,这种传输实现允许令牌单独发送。这里的基本机制只是更新id_to_owner映射,跟踪谁拥有什么。从这里开始,发件人和收件人owner_to_token_count记录也会更新,以跟踪个人帐户所拥有的令牌。
NFTOKEN的最后一个功能是能够批准另一个帐户代表您发送令牌。让我们看看这是如何工作的。
批准实施
批准是一种机制,通过该机制,令牌所有者可以指派其他人代表他们转移令牌。 为此,专门用于批准或拒绝特定token_id的帐户的附加功能。
注意:在进一步开发Ink语言之前,合同目前仅限于每个令牌一次批准。
公共approvals()函数在被调用时接受三个参数:
我们正在添加或删除批准的token_id
我们希望批准或拒绝AccountId
批准的布尔值,表示我们是要批准还是拒绝AccountId
Approvals()的签名如下所示:
审批流程归结为以下逻辑,确保只有令牌所有者才能进行审批,并且可以成功插入或删除审批:
我们首先通过id_to_owner映射检查令牌的所有者是否存在。 这里的None值表明令牌不存在,在这种情况下我们退出该函数。
在成功获取令牌的情况下,我们检查调用者(env.caller())是否确实是我们正在配置批准的令牌的所有者。 如果没有,我们退出该函数,再次返回false。
接下来,我们尝试获取现有的批准记录:
如果不存在批准记录,我们会参考批准,以查看调用者是打算批准还是拒绝提供的帐户。 如果调用者确实希望批准,则将提供的帐户添加到批准中。 如果没有,将无法删除任何内容,因为找不到记录 - 我们返回false。
如果存在批准记录,则使用unwrap()解包该值,并再次检查调用者的意图。 如果打算拒登,我们会通过HashMap remove()方法从批准中删除记录。 另一方面,如果调用者打算插入(或更新)批准,我们会再次插入记录,覆盖现有记录。
最后,发出Approval事件,我们返回true:
最后,以上是关于Ink NFToken合同实施的讨论。
本文转载公众号:区块链研究实验室,专注区块链技术,产品社群,经济模型等全方位的知识体系输出,为大家带来不一样的社群学习体验。欢迎联系作者微信加入社群:csschan1120