以太坊不仅是一种加密数字货币,它更是功能完备的智能合约平台,solidity就是用来开发以太坊上的智能合约的原生开发语言。solidity最早发布于2015年,它是第一种图灵完备的智能合约专用开发语言。目前除了以太坊之外,在其他区块链中也逐渐开始支持solidity,例如hyperledger fabric、tendermint等。在这个solidity快速教程中,我们将使用最新0.5版的solidity,以一个具体的案例来介绍solidity智能合约的开发、部署与交互,希望对你快速掌握solidity智能合约的开发有所帮助。
如果要高效系统地掌握以太坊智能合约与DApp的开发,推荐访问汇智网的在线互动课程:
以太坊开发入门 | java以太坊 | python以太坊 | php以太坊 | C#以太坊 | 电商DApp实战 | ERC721通证实战
0、问题的背景
有一个老爷爷,在生命的最后岁月别无他求,只是希望自己的财产能够通过遗嘱顺利地传给其他家庭成员。
在传统的遗嘱中,遗产分配方案是落实在法律文件上的,然后当真正开始分配时,法官需要重审文件并做出相应的决定。常见的问题发生在家庭成员之间对分配比例的争执上,甚至因此而导致家庭成员关系的破裂。在法庭听证阶段,这些都会影响法官最终的裁决,并因此可能导致不公平的结果,甚至对家庭关系造成进一步的伤害。
那么,如果我们可以让遗产分配自动进行,是否可以避免上述情况的发生?
如果遗产是一个智能合约,那么就不需要法官了。老爷爷可以自主地利用合约管理资产,然后在他去世后由程序来分配遗产给家庭成员。合约里的代码就决定了最终的分配结果,因此无需法官的介入。例如萨拉分$10000,本得到$5000,朱丽叶得到$2000。代码执行后,资产以代币或加密货币的形式自动分配给这些家庭成员,而无需人工介入。虽然不能保证每个成员都对遗产的分配结果满意,但是没有人会和代码争执。这听起来还比较可行,对吗?
记住这个案例,在这个快速教程中,我们将使用solidity,为老爷爷开发一个简单的遗嘱合约,来满足他最后的愿望。
1、搭建solidity开发环境
开发solidity智能合约最简单的方法,就是使用官方提供的在线集成开发环境REMIX,你可以点击这里打开remix,在网页里就完成solidity智能合约的编写、编译与部署:
在你打开remix页面后,注意在右侧的run
选项页,environment
下拉框中,要选中JavaScript VM
。这个选项的意思是使用一个内存仿真以太坊节点作为你的solidity智能合约的运行平台,这样就不用考虑与实际的以太坊主网交互所需要的账号、资金、计算费用等问题,而可以先把精力聚焦在学习如何使用solidity表达你的业务逻辑上。
点击remix页面左上方的+
图标,就可以创建一个新的代码文件,我们将其命名为will.sol。在remix页面中间的编辑区域可以同时显示多个文件,当前正在编辑的文件,则以活动选项页的形式显示文件名称。
2、声明solidity编译器版本
solidity还是很早期阶段的语言,从语法到编译器都在不断地演化,所以在solidity代码的第一行,一定要用pragma关键字声明这个文件中的solidity代码需要哪个版本的编译器。例如:
注意在solidity中,末尾的分号不可省略。
3、编写第一个solidity合约
接下来就可以定义我们的第一个合约:
使用contract关键字来定义一个合约,solidity的合约类似于我们熟悉的OOP中的类,因此通常合约的名称首字母也会大写,例如Will
。一对大括号用来定义合约的实现
逻辑,单行注释也使用//
,这和很多开发语言都类似。
4、solidity中的全局变量和构造函数
在我们开始写代码之前,应当首先明确遗嘱的条款。假设老爷爷的遗产是50个以太币,其中20个留给他的儿子康莱德,剩下的30个留给他的妻子丽莎。在真实的环境中,当老爷爷去世后,应当有一个外部的程序将调用合约中定义的方法来分配遗产,但是我们为了便于学习将自己完成这个调用。
现在,让我们先完成如下代码:
- 表征合约所有者的变量
- 表征遗产数量的变量
- 表征老爷爷是否还健在的变量
- 设置上述变量初始值的构造函数
第5行代码定义了合约的所有者。当我们在solidity中定义变量时,必须先声明其类型。address
是solidity中一种特殊的类型,它表示一个以太坊地址。address
类型的变量有一些特殊的方法,我们在后面会进一步了解。
第6行代码定义的fortune变量用来保存老爷爷的遗产数量,它的类型是uint
或unsigned int
,意思是这个变量是0或正整数。solidity中有很多数据类型,但我们不会在这里一一介绍,你可以在官方文档中深入了解solidity的数据类型。
第7行代码定义的isDeceased变量用来标识老爷爷是否已经去世,这是一个开关量,因此其类型为boolean
,可能的值只有两个:true或false,默认值为false。
第9~13行代码是合约的构造函数,这个特殊的函数将在合约部署的时候自动执行。
public
关键字被称为可见性修饰符,它的作用是声明被修饰的方法是否允许外部调用。public
意味着在合约内部或外部(由其他合约或其他人)都可以调用该方法。
payable
关键字是solidity的特色之一,它使得被修饰的方法可以发送或接收以太币。为构造函数声明payable
关键字意味着当我们部署合约的时候,可以直接向合约存入以太币,例如,作为遗产的50个以太币。当合约接收到以太币后,这些币就保存在合约
地址上了。
在构造函数内部,我们将owner
变量的值设置为msg.sender
,这是一个以太坊平台预置的全局变量,表示调用合约方法的账号地址,在我们的案例中,这个地址是老爷爷的。
同时我们将fortune
变量的值设置为msg.value
,这是另一个全局变量,它表示被调用的方法接收到的以太币的数量。
虽然变量isDeceased
被自动初始化为默认值false,但为了清晰起见,我们将其显式地设置为false。
5、使用solidity修饰符
在solidity中,修饰符(Modifier)可以为函数附加额外的条件逻辑。例如,假设我有一个用来关灯的方法,同时有一个修饰符要求灯开关必须处于on状态,那么我们就可以在方法上附加声明这个修饰符,以便确保只有在灯开关处于on状态时,才可以调用这个方法,否则就抛出异常。
第15行代码定义了onlyOwner
修饰符。如果一个方法附加声明了这个修饰符,那么就要求调用方法的账号(msg.sender)必须与owner
变量的值一致(别忘了我们在构造函数中设置了owner的值)。这个调用条件有助于遗产的分配,我们将在后面看到这一点。
require
关键字的意思是,括号里的表达式的值必须为真(true),否则就会抛出异常,不再继续执行代码。
_;
起到占位符的作用,在执行过程中,以太坊虚拟机会用被修饰的方法代码来替换它。
第20行代码定义了mustBeDeceased
修饰符。如果一个方法附加声明了这个修饰符,那么就只有在isDeceased
变量值为true时,才可以调用该方法,否则就抛出异常。
在上面的代码中,我们使用修饰符来限定方法的执行条件,当然也可以不使用修饰符,而直接在方法实现代码中使用require
,不过修饰符看起来更高级一些,也更容易实现代码的复用。
6、设定遗产分配方案
现在我们要继续完成遗产在家庭成员之间的分配任务,这需要他们的钱包地址和分配数量。
正如我们之前所述,康莱德将收到20个以太币而丽莎将继承30个。让我们创建一个数组来保存他们的钱包地址,然后写一个方法来分配遗产。
第25行代码定义了一个空数组familyWallets
,用来保存所有家庭成员的钱包地址。和其他语言一样,在solidity中数组是顺序存放并且可以使用序号来存取。注意方括号之前的关键字paybale
,只有address payable
类型的变量,才可以接收以太币,这是0.5版本的solidity与之前版本的区别之一。
第27行代码创建了一个从address
类型到uint
类型的映射表变量inheritance
,用来保存每个钱包地址的遗产数量。这是一个键/值对数据结构,类似于其他语言中的字典或哈希表,可以用键来存取值。
第29行代码定义了一个方法,它的功能是将一个钱包地址添加到familyWallets
数组,然后设置该地址在inheritance
映射表中的遗产数量。注意附加的onlyOwner
修饰符,猜一下为什么我们要在这里声明这个修饰符?
第30行代码将传入方法的钱包地址追加到familyWallets
数组的末尾。
第31行代码将传入方法的遗产继承数量设置为映射表inheritance
的指定地址(传入方法的另一个参数)的值。
7、实现遗产自动分配
让我们总结一下。到目前为止,我们已经学习了全局变量、数据类型、构造函数、特殊的关键字例如payable
和public
、内置的全局变量例如msg.sender
和msg.value
、修饰符和require
、数组、映射表和方法。我们已经搭好了合约的框架,现在让我们把各部分整合起来最终完成合约。
作为这个教程最后一部分的代码,我们将实现家庭成员遗产的自动分配。
第34行定义了payout()
方法,注意private
关键字,这个可视性修饰符是public
的反义词,它只允许被修饰的方法在合约内部调用,就像在第42行的代码那样。之所以在这里使用private
,主要是考虑到安全性,因为我们
不希望任何来自合约外部的调用。注意最后的mustBeDeceased
修饰符,目前我们依然不能满足这个修饰符要求的条件来执行payout()
方法。
第35行代码是一个for
循环,用来遍历familyWallets
数组。语法如下:
- 定义一个计数器变量i,
- 声明循环的执行条件
- 每个周期计数器变量i加1
第36行代码是整个合约的核心,我们调用address
类型的地址对象的transfer()
方法,向该地址转账预定的遗产继承数量,inheritance[familyWallets[i]]
表示在inheritance
映射表中,键familyWallets[i]
的值,也就是第i个家庭成员的遗产继承数量。
第40~42行代码定义了一个方法,当老爷爷去世后将调用这个方法来触发遗产的分配。在这里我们将变量isDeceased
的值设置为true。
现在我们完成了吗?
实际上,还不完全是...
这个智能合约的代码是写完了,但是我们怎么用它?现在是收获果实的时候了。
8、solidity合约部署与交互
你的remix页面看起来应该像这样:
在remix页面右边切换到compile
选项页,确认按下图选中编译器的版本,然后点击[start to compile]:
你可能会看到静态分析生成的一个蓝色文本框,我们暂时忽略它的提醒,切换到run
选项页:
确保Environment
下拉框中选中了Javascript VM
,点击account
的下拉菜单将显示5个测试账户,每个账户都有100个以太币,让我们选择第一个。
向以太坊区块链部署合约并不是免费的,部署者需要支付手续费,通常被称为gas。引入这一机制的目的是避免区块链计算资源被恶意滥用,要进一步了解gas,可以查看这篇文章:1分钟搞清Gas/ Gas Price/ Gas Limit。
gas limit
字段使用默认值就可以了,我们先不修改它。
value
字段表示我们在部署合约时要发送给合约的以太币数量。输入50,还记得
我们在定义构造函数时附加的payable
关键字吗?
现在继续,点击[deploy]。
你可能立刻会注意到3件事。首先,选中的账户余额现在变成了49.9999… ,这是因为
我们转给合约50个以太币,还要扣除一点部署手续费。页面底部的控制台也会提供
关于部署过程的详细信息,你可以查看一下。现在看起来是这样:
我们的合约已经成功部署了!它生成了自己的地址,并且显示出我们定义的两个合约方法。作为合约的持有者,我们要做的第一件事,是设置家庭成员的继承数量:康莱德(20)、丽莎(30)。假设我们用account
下拉菜单中的第二个作为康莱德的账号,丽莎的用第三个。
选择第二个账号,点击[拷贝到剪切板]图标,然后输入上图中的setInheritance
后面的文本输入框。
在我们执行setInheritance
方法之前,有几件事情要记住。
传入合约的以太币数量的单位是wei而不是以太币,1 ETH = 1,000,000,000,000,000,000 WEI,这是非常小的单位,因此我们需要将以太币表示的遗产数量先转换为以WEI为单位的值。
在将遗产数量换算后,在将其写入上图中的setInheritance
后面的文本输入框中,之前输入的地址后面,这两个值之间注意要用逗号隔开。
还有,别忘了在account
下拉框选中第一个账号,还记得onlyOwner
修饰符吗?只有合约的持有人才可以调用setInheritance
方法!
现在让我们依次为康莱德和丽莎执行setInheritance方法。你应当可以看到控制台输出的成功信息。看一下其中的decoded input
:
你看,它显示的就是我们输入的数据。
遗产分配好了,但是坏消息来了。老爷爷在73岁时,在一次北极探险中不幸因心脏病
突发去世。他总是这么充满激情与活力。
当我们纪念这位老爷爷的同时,我们同时调用遗嘱合约的deceased()
方法,完成
老爷爷的最后的愿望。。。