简介

智能合约是现在区块链的一大特色,而不同的链使用的智能合约的虚拟机各不相同,编码语言也有很大差异。而今天我们开始学习EOS的智能合约,我也是从EOS初期一直开发合约至今,期间踩过无数坑,也在Stack Overflow上提过问(最后自己解决了),在实际生产中也积累了很多经验,所以我会连续几周分多次分享合约开发的经验,今天先来点基础的。

一些C++的编程基础

EOS就是使用C++开发的,这也为它带来了诸多好处,而合约也沿用C++作为开发语言,虽然合约中无法直接使用Boost等框架(你可以自己引入,但这也意味着合约会很大,会占用大量账号的内存),但是我们还是可以使用很多C++的小型库,并伴随着eosio.cdt的发展,融入了更多实用的合约功能。

如果你之前没有使用C系列的开发语言做过开发,比如:C语言、C++或者是C#,那么你需要先学习下C语言的基本语法和数据结构,这里我不做展开,在我们的系列文章的开篇就介绍了我推荐的Learn EOS - c/c++ 教程英文版,有一定英语基础的朋友可以直接看这个,其他朋友也可以在网上找一些C++的入门教程看下。

如果你已经有了一定的C语言基础,那么写合约的话,你会发现需要的基础也并不多,依葫芦画瓢就能写出各种基础功能了,所以,你并不需要担心太多语言上的门槛,毕竟合约只是一个特定环境下运行的程序,你能用到的东西并不会很多。

CDT选择

EOS的早期版本进行合约开发还没有CDT工具,那时的合约借助的是源码中的工具eosiocpp,所以你看2018年的博客,进行合约编译都是用它,但你现在是见不到了。随着官方CDT的迭代,在CDT的1.4版本开始被官方推荐使用,CDT后面也经历了几个大的版本更新,逐步改善合约编写方式,更加趋于简洁、直观。

但是不同的CDT版本,也意味着编译器的不同,所以合约开发也会有所区别,比如一些语法变了,一些库名称变了,增加了一些新的标注……

我们的教程侧重还是介绍最新的语法,所以推荐使用1.6以上的版本。我也会尽量在后面的介绍中补充说明老的CDT的写法,方便大家对照网上其他老博客的合约。

来个HelloWorld

学习任何编程,我们都不能少了Mr.HelloWorld,先来给大家打个招呼吧。

#include <eosio/eosio.hpp>

using namespace eosio;

class [[eosio::contract]] hello : public contract
{
public:
using contract::contract; [[eosio::action]] void hi(name user)
{
print("Hello, ", user);
}
};

  

基本合约结构及类型

hello合约就是一个最简单的合约了,而且还有一个可调用的action为hi。我们首先还是来介绍下一个合约的程序结构吧。

  • 程序头

包含了引入的头文件、库文件等,还有全局的命名空间的引入等。

#include <eosio/eosio.hpp>

using namespace eosio;

  

这里eosio库是我们的合约基础库,所有和eos相关的类型和方法,都在这个库里面,而这个库里面eosio.hpp是基础,包含了contract等的定义,所以所有的合约都要引入。

【CDT老版本】早期cdt版本中库名称不是eosio,而是eosiolib

默认的,我们引入了eosio的命名空间,因为eosio的所有内容都是在这个命名空间下的,所以我们全局引入,会方便我们后续的代码编写。

  • 合约类定义

其实就是定义了一个class,继承contract,并通过[[eosio::contract]]标注这个类是一个合约。使用using引入contract也是为了后续代码可以更简洁。

class [[eosio::contract]] hello : public contract{
public:
using contract::contract;
}

  

【CDT老版本】早期cdt版本中直接使用了CONTRACT来定义合约类,比如:CONTRACT hello: public contract {}

  • action定义

写一个public的方法,参数尽量用简单或者是eosio内置的类型定义,无返回值(合约调用无法返回任何结果,除非报错),然后在用[[eosio::action]]标注这个方法是一个合约action就行。

注意:action的名称要求符合name类型的规则,name规则请看下面的常用类型中的说明。

[[eosio::action]]
void hi( name user ) {
print( "Hello, ", user);
}

  

因为合约无法调试,所以只能通过print来打印信息,或者直接通过断言抛出异常来进行调试。

【CDT老版本】早期cdt版本中直接使用ACTION来定义方法,比如:ACTION hi( name user ){}

  • 常用类型
类型 说明 示例
name 名称类型,账号名、表名、action名都是该类型,只能使用26个小写字母和1到5的数字,特殊可以使用小数点,总长不超过13。 name("hi") 或者 "hi"_n
asset 资产类型,Token都是使用该类型,包含了Token符号和小数位,是一个复合类型,字符形式为1.0000 EOS asset(10000, symbol("TADO", 4)就是1.0000 TADO)
uint64_t 无符号64位整型,主要数据类型,表主键、name实质都是改类型 uint64_t amount = 10000000;
  • 内置常用对象或方法

在合约中,contract基类提供了一些方便的内置对象。

首先是get_self()或者是_self,这个方法可以获取到当前合约所在的账号,比如你把hello合约部署到了helloworld111这个账号,那么get_self()就可以获取到helloworld111。

然后是get_code()或者是_code,这个方法可以获取到当前交易请求的action方法名,这个在进行内联action调用时可以用于判断入口action。

最后是get_datastream()或者_ds,这个方法获取的是数据流,如果你使用的是复杂类型,或者是自定义类型,那么你无法在方法的参数上直接获取到反序列化的变量值,你必须自己通过数据流来解析。

常用的还有获取当前时间current_time_point(),这个需要引入#include <eosio/transaction.hpp>

数据持久化

当然,合约里面,我们总会有些功能需要把数据存下来,在链上持久化存储。所以我们就需要定义合约表了。

合约的表存在相应的合约账号中,可以划分表范围(scope),每个表都有一个主键,uint64_t类型的,还可以有多个其他索引,表的查询都是基于索引的。

这里先提一句,表数据所占用的内存,默认是合约账号的内存,也可以使用其他账号的,但需要权限,这个以后我们再介绍。

我们扩展一下hello合约。

#include <eosio/eosio.hpp>
#include <eosio/transaction.hpp> using namespace eosio; class [[eosio::contract]] hello : public contract
{
public:
using contract::contract; hello(name receiver, name code, datastream<const char *> ds) : contract(receiver, code, ds), friend_table(get_self(), get_self().value)
{
} [[eosio::action]] void hi(name user)
{
print("Hello, ", user); uint32_t now = current_time_point().sec_since_epoch(); auto friend_itr = friend_table.find(user.value);
if (friend_itr == friend_table.end())
{
friend_table.emplace(get_self(), [&](auto &f) {
f.friend_name = user;
f.visit_time = now;
});
}
else
{
friend_table.modify(friend_itr, get_self(), [&](auto &f) {
f.visit_time = now;
});
}
} [[eosio::action]] void nevermeet(name user)
{
print("Never see you again, ", user); auto friend_itr = friend_table.find(user.value);
check(friend_itr != friend_table.end(), "I don't know who you are."); friend_table.erase(friend_itr);
} private:
struct [[eosio::table]] my_friend
{
name friend_name;
uint64_t visit_time; uint64_t primary_key() const { return friend_name.value; }
}; typedef eosio::multi_index<"friends"_n, my_friend> friends; friends friend_table;
};

  

可以看到,我们已经扩充了不少东西了,包括构造函数,表定义,多索引表配置,并完善了原先的hi方法,增加了nevermeet方法。

我们现在模拟的是这样一个使用场景,我们遇到一个朋友的时候,就会和他打招呼(调用hi),如果这个朋友是一个新朋友,就会插入一条记录到我们的朋友表中,如果是一个老朋友了,我们就会更新这个朋友的记录中的访问时间。当我们决定不再见这个朋友了,就是绝交了(调用nevermeet),我们就会把这个朋友的记录删除。

  • 表定义

首先我们需要声明我们的朋友表。定义一个结构体,然后用[[eosio::table]]标注这个结构体是一个合约表。在结构体里定义一个函数名primary_key,返回uint64_t类型,作为主键的定义。

private:
struct [[eosio::table]] my_friend
{
name friend_name;
uint64_t visit_time; uint64_t primary_key() const { return friend_name.value; }
};

  

我们这里声明了一个my_friend的表,合约的表名不在这里定义,所以结构体的名称不必满足name的规则。我们定义了两个字段,friend_name(朋友的名称)和visit_time(拜访时间),主键我们直接使用了friend_name,这个字段是name类型的,而name类型的实质就是一个uint64_t的类型(所以name的规则那么苛刻)。

【CDT老版本】早期cdt版本中直接使用TABLE来定义合约表,比如:TABLE my_friend{}

  • 多索引表配置

合约里的表都是通过多索引来定义的,这是合约表的结构基础。所以这里才是定义表名和查询索引的地方。

typedef eosio::multi_index<"friends"_n, my_friend> friends;

  

我们现在只介绍最简单的单索引的定义,以后再介绍多索引的定义方式,这里的"friends"_n就是定义表名,所以使用了name类型,之后my_friend是表的结构类型,typedef实质上就是声明了一个类型别名,名字是friends的类型。

  • 构造函数

构造函数这里并不是必须,但是为了我们能在全局直接使用合约表,所以我们要在构造函数进行表对象的实例化。

public:
hello(name receiver, name code, datastream<const char *> ds) : contract(receiver, code, ds), friend_table(get_self(), get_self().value)
{
} private:
friends friend_table;

  

这一段是标准合约构造函数,hello(name receiver, name code, datastream<const char *> ds) : contract(receiver, code, ds),合约类型实例化时会传入receiver也就是我们的合约账号(一般情况下),code就是我们的action名称,ds就是数据流。

friend_table(get_self(), get_self().value)这一段就是对我们定义的friend_table变量的实例化,friend_table变量就是我们定义的多索引表的friends类型的实例。在合约里我们就可以直接使用friend_table变量来进行表操作了。实例化时传递的两个参数正是表所在合约的名称和表范围(scope),这里都使用的是当前合约的名称。

  • 查询记录

查询有多种方式,也就是多索引表提供了多种查询的方式,默认的,使用findget方法是直接使用主键进行查询,下次我们会介绍使用第二、第三等索引来进行查询。find返回的是指针,数据是否存在,需要通过判断指针是否是指到了表末尾,如果等于表末尾,就说明数据不存在,否则,指针的值就是数据对象。get直接返回的就是数据对象,所以在调用get时,就必须传递数据不存在时的错误信息。

auto friend_itr = friend_table.find(user.value);
if (friend_itr == friend_table.end())
{
//数据不存在
}else
{
//数据存在
}

  

我们在hi方法中先查询了user是否存在。如果不存在,我们就添加数据,如果存在了,就修改数据中的visit_time字段的值为当前时间。

  • 添加记录

多索引的表对象添加记录使用emplace方法,第一个参数就是内存使用的对象,第二个参数就是添加表对象时的委托方法。

uint32_t now = current_time_point().sec_since_epoch();

auto friend_itr = friend_table.find(user.value);
if (friend_itr == friend_table.end())
{
friend_table.emplace(get_self(), [&](auto &f) {
f.friend_name = user;
f.visit_time = now;
});
}
else
{
//数据存在
}

  

这里先定义了一个变量now来表示当前时间,正是使用的内置方法current_time_point(),这个还是用了它的sec_since_epoch()方法,是为了直接获取秒单位的值。

我们查询后发现这个user的数据不存在,所以就进行插入操作,内存直接使用的合约账号的,所以使用get_self(),然后对表数据对象进行赋值。

  • 修改记录

多索引的表对象修改记录使用modify方法,第一个参数是传递需要修改的数据指针,第二个参数是内存使用的对象,第二个参数就是表对象修改时的委托方法。

friend_table.modify(friend_itr, get_self(), [&](auto &f) {
f.visit_time = now;
});

  

我们将查询到的用户对象的指针friend_itr传入,然后内存还是使用合约账号的,委托中,我们只修改visit_time的值(主键是不能修改的)。

  • 删除记录
  • 多索引的表对象删除记录使用erase方法,只有一个参数,就是要删除的对象指针,有返回值,是删除数据后的指针偏移,也就是下一条数据的指针。
auto friend_itr = friend_table.find(user.value);
check(friend_itr != friend_table.end(), "I don't know who you are."); friend_table.erase(friend_itr);

  

我们的示例中,将查询到的这条数据直接删除,并为使用变量来接收下一条数据的指针,在连续删除数据时,你会需要获取下一条数据的指针,因为已删除的数据的指针已经失效了。

编译

编译我们再之前也有过介绍,安装了eosio.cdt后,我们就有了eosio-cpp命令,进入到合约文件夹中,直接执行以下命令就会在当前目录生成wasm和abi文件。

eosio-cpp -abigen hello.cpp -o hello.wasm

注意:替换命令中使用的hello.cpp为实际合约代码文件名,而hello.wasm为实际合约的wasm文件名。

当然,编译不通过的时候,你就要看看错误是什么了,这可能会考验一下你的C++功底。

发布

决定了要发布的账号后,记得要购买足够的内存和抵押足够的资源。合约的内存消耗我们可以大致这样估算,看下编译好了的合约wasm文件有多大,然后乘以10,就是你发布到链上大概所需的内存大小了。

发布合约我们使用cleos set contract命令,其后跟合约账号名和合约目录,为了方便,我建议你把合约的目录名保持和合约文件名一致。

cleos set contract helloworld111 ./hello -p helloworld111

这里我们给出的代码是将hello目录下的hello合约发布到helloworld111。我这里的文件夹是hello,里面的abi和wasm也都是hello,这样你不用手动指定合约文件了。

总结

至此,我想大家应该对合约的编写有了一个大致的了解了,至少你可以参照着写个简单的合约出来了,这其中还有很多技巧和高级用法,我会在后续的文章中继续和大家分享。

EOS基础全家桶(十三)智能合约基础的更多相关文章

  1. EOS基础全家桶(十二)智能合约IDE-VSCode

    简介 上一篇我们介绍了EOS的专用IDE工具EOS Studio,该工具的优势是简单,易上手,但是灵活性低,且对系统资源开销大,依赖多,容易出现功能异常.那么我们开发人员最容易使用的,可能还是深度定制 ...

  2. EOS基础全家桶(十四)智能合约进阶

    简介 通过上一期的学习,大家应该能写一些简单的功能了,但是在实际生产中的功能需求往往要复杂很多,今天我就继续和大家分享下智能合约中的一些高级用法和功能. 使用docker编译 如果你需要使用不同版本的 ...

  3. EOS基础全家桶(七)合约表操作

    简介 本篇我们开始来为后续合约开发做准备了,先来说说EOS内置的系统合约的功能吧,本篇将侧重于合约表数据的查询,这将有利于我们理解EOS的功能,并可以进行必要的数据查询. EOS基础全家桶(七)合约表 ...

  4. EOS基础全家桶(六)账号管理

    简介 本篇我们会学习最基本的账号相关的操作,包括了创建账号和查询,关于账号资源的操作因为必须先部署系统合约,所以我们会留到后面单独写一篇来讲解. 6-EOS基础全家桶(六)账号管理 简介 账号介绍 账 ...

  5. EOS基础全家桶(十)交易Action操作

    简介 区块链上的所有操作都是通过交易(Transaction)上链的,无论你是转账交易还是发起的智能合约的调用,而EOS和传统区块链不同的是EOS在一个交易里可以发起多个行为(Action),这使得E ...

  6. EOS基础全家桶(八)jungle测试网的使用

    简介 前面我们已经学习了一些EOS的基础知识了,但是在EOS主网上的很多操作(比如:抵押.赎回.买卖内存)都是需要EOS链被正式激活后才可使用,而激活EOS链还需要很多的准备操作,我打算在单独的一篇文 ...

  7. EOS基础全家桶(五)钱包管理

    简介 本篇我们将会学习EOS自带的命令行钱包的使用方法,我们将会使用cleos来控制keosd服务对本地钱包进行管理. 虽然现在市面上已经有很多支持EOS的钱包了,有Web钱包,有app钱包,还有浏览 ...

  8. EOS基础全家桶(十一)智能合约IDE-EOS_Studio

    简介 我们马上要进入智能合约的开发了,以太坊最初提供了智能合约的功能,并宣告区块链进入2.0时代,而EOS的智能合约更进一步,提供了更多的便利性和可能性.为了进一步了解智能合约,并进行开发,我们需要先 ...

  9. EOS基础全家桶(十五)智能合约进阶2

    简介 今天我们继续补充智能合约的进阶使用技巧,这次的主题是交易,合约内我们除了可以发起内联action的调用,很多使用还需要直接调用其他的合约action或者以交易的形式调用自身的action. 发起 ...

随机推荐

  1. shiro简单的认证功能

    使用静态shiro.ini文件完成认证 创建项目到爆 <dependency> <groupId>org.apache.shiro</groupId> <ar ...

  2. 最全的ASCII码对照表

    转自https://blog.csdn.net/jinduozhao/article/details/75398793 十进制代码 十六进制代码 MCS 字符或缩写 DEC 多国字符名 ASCII 控 ...

  3. Java——native关键字

    说明:在使用HashSet的过程中,查看Object.java过程中发现hashCode()方法是以native关键字修饰,没看到过该关键字,这里记录下来. native关键字用来修饰方法,是使用一些 ...

  4. 个人工具,编辑器visual studio code

    个人收集的使用方法:简化版 主要基于基础web前端开发,visual studio code教程——基础使用.扩展插件安装使用 下载地址: https://visualstudio.microsoft ...

  5. 这些Java8官方挖过的坑,你踩过几个?

    导读:系统启动异常日志竟然被JDK吞噬无法定位?同样的加密方法,竟然出现部分数据解密失败?往List里面添加数据竟然提示不支持?日期明明间隔1年却输出1天,难不成这是天上人间?1582年神秘消失的10 ...

  6. Keycloak快速上手指南,只需10分钟即可接入Spring Boot/Vue前后端分离应用实现SSO单点登录

    登录及身份认证是现代web应用最基本的功能之一,对于企业内部的系统,多个系统往往希望有一套SSO服务对企业用户的登录及身份认证进行统一的管理,提升用户同时使用多个系统的体验,Keycloak正是为此种 ...

  7. Beta冲刺 —— 5.31

    这个作业属于哪个课程 软件工程 这个作业要求在哪里 Beta冲刺 这个作业的目标 Beta冲刺 作业正文 正文 github链接 项目地址 其他参考文献 无 一.会议内容 1.讨论并解决每个人存在的问 ...

  8. Java实现 LeetCode 523 连续的子数组和(ง •_•)ง

    523. 连续的子数组和 给定一个包含非负数的数组和一个目标整数 k,编写一个函数来判断该数组是否含有连续的子数组,其大小至少为 2,总和为 k 的倍数,即总和为 n*k,其中 n 也是一个整数. 示 ...

  9. java实现第N个素数

    素数就是不能再进行等分的整数.比如:7,11.而9不是素数,因为它可以平分为3等份.一般认为最小的素数是2,接着是3,5,... 请问,第100002(十万零二)个素数是多少? 请注意:2 是第一素数 ...

  10. 卷积生成对抗网络(DCGAN)---生成手写数字

    深度卷积生成对抗网络(DCGAN) ---- 生成 MNIST 手写图片 1.基本原理 生成对抗网络(GAN)由2个重要的部分构成: 生成器(Generator):通过机器生成数据(大部分情况下是图像 ...