- A+
原文: https://ethfans.org/posts/introduction-smart-contract-development
我最近在 Blockgeeks 上读了一篇好文章,在这篇文章里 Ameer 尝试把关于去中心化应用(DApp)编码的所有知识都放到其中。这是一件多么伟大的工作啊!当然这里边有很多要素可以讨论,我会详细阐述其中的一些工具,以及它们是如何用于智能合约开发的。
这一系列的文章不是为智能合约 101 或以太坊平台写的。当然你可以在 Blockgeeks 中发现许多有用的东西。但是我确定,通过浏览这篇文章,你会对这些工具有所了解,我也希望这会对你使用 Solidity 编写智能合约有所帮助。
我首先简要介绍我所选的智能合约。然后在四种环境中部署这个合约。以下是四种环境的概要。
下图为合约编译和部署的简易流程。
智能合约例子:收益共享
此应用摘自 A. Bahga 和 V. Madisetti 撰写的 Blockchain Applications: A Hands-On Approach 一书第4章:“以太坊账户”中的智能合约。 它已经按照最新版 Solidity 的要求修改过了。
这个应用被称为收益共享。简而言之,在合约部署时会产生一组给定的地址。任何人都可以发送一定数量的资金(这里指的是以太币或者低面额单位的以太币),这些资金会被平均分配到每一个地址上。这是一个相当简单的Solidity智能合约。
这是智能合约的Solidity代码。
pragma solidity ^0.4.8; contract RevenueSharing { address public creator; mapping(uint => address) public shareholders; uint public numShareholders; event Disburse(uint _amount, uint _numShareholders); function RevenueSharing(address[] addresses) { creator = msg.sender; numShareholders = addresses.length; for (uint i=0; i< addresses.length; i++) { shareholders[i] = addresses[i]; } } function shareRevenue() payable returns (bool success) { uint amount = msg.value / numShareholders; for (uint i=0; i<numshareholders; i++)="" {="" if="" (!shareholders[i].send(amount))="" revert();="" }="" disburse(msg.value,="" numshareholders);="" return="" true;="" function="" kill()="" (msg.sender="=" creator)="" selfdestruct(creator);="" }
这个智能合约的一些要点:
-
这个合约被命名为收益共享。
-
RevenueSharing() 函数与智能合约有相同的名字。它是构造函数,只会在合约部署时被调用一次。我们可以看到,这个合约通过构造函数接收了一个地址数组,并且这个地址数组是保存在另一个叫做 shareholders 的数组中的。
-
shareRevenue() 函数是这个智能合约中唯一的主函数。当以一定量的以太币(由 msg.value 所指定)来执行这个函数的时候,这些以太币会按照shareholders的数量(即 numShareholders 的值)均分,shareholder 数组中的每个地址都会得到其中一份。我们在演示中会执行这个函数。
-
kill() 函数用于移除智能合约,我们在演示中不会用到这个函数。
注意,所有的变量都被定义为 public 变量。这有助于我们观察到合约中的更多细节。在实际编程中,出于安全考虑,当我们把变量或函数设为 public 时要非常小心。
Remix
概览
Remix is a suite of tools to interact with the Ethereum blockchain in order to debug transactions (directly quoted from here). There is an IDE version (Remix IDE) and an online version, which we will use here.
Remix 是一套与以太坊区块链进行交互来调试交易(由此处直接引用)的工具。有 IDE 版本(Remix IDE)和在线版本,我们用的是在线版本。
Remix中有许多工具,但我们只对以下工具感兴趣:
-
Solidity编译器。它会生成我们将在另一个环境下用到的许多有用的信息。
-
运行环境。Remix提供了三个:
-
嵌入的Web3:例如由Mist或者MetaMask所提供的
-
Web3提供者:通过IPC从本机获取
-
JavaScript虚拟机:一个模拟环境
在这几个运行环境中,我们使用 JavaScript 虚拟机,在 JavaScript 虚拟机中,Remix 由 5 个以太坊账户组成,每个账户都存有 100 以太币。这对于测试我们的合约来说就足够了。而且,挖矿是不需要的,因为它是自动完成的。
你可以很容易的从任意浏览器上下载Remix(下载连接:http://remix.ethereum.org)。这是Remix的屏幕截图。
屏幕被分成了几个区:
-
智能合约区:我们在这里粘贴智能合约的Solidity代码。
-
编译和运行区:在编译标签中,展示了所有的编译错误和警告。在运行标签中,我们部署合约并且执行合约函数。
-
交易日志区:所有的交易细节都可以在这里看到。
合约编译
我们把智能合约代码粘贴到 Remix 中。
我们可以看到,代码是自动编译的,并且有一些警告。由于这并不是什么重大的错误,我们可以安全地继续进行下一步。
如果我们点击Details按钮,我们可以看到这个合约的许多信息。其中:
-
Bytecode 字节码
-
ABI 应用二进制接口
-
Web3 Deploy Web3Deploy
在另一个环境部署这个合约时,这些信息都是需要的。我们一会儿再回到这里。
因为编译没有错误,我们可以在Remix提供的JavaScript虚拟机环境下运行这个合约。
合约部署
1.Run标签一瞥
Gas 限制是为了指明我们能在任一交易中花费的 gas 数量。由于我们处于测试环境中,所以并不太担心这一点。我已经试过一些大的合约部署,并且默认给出的 gas limit 都不合适。 不过,这个值可以在需要时随意增加。
Value 部分是我们在合约部署和执行一个函数时发送的以太币的数量。在我们这个例子中,部署合约不需要设定以太币,但执行函数需要设定以太币。更多细节请往下看。
2.部署合约
现在我们可以看到收益共享合约已经被选中(我们的代码中只有这一个合约)。我们会使用 Create 按钮来把这个合约部署到 JavaScript 虚拟机上。
就像输入区中提示的那样,在部署合约时需要指定 “address[] addresses”。还记得这个合约需要一个地址列表作为共享目标吗?出于演示目的,我们将使用上面列出的第3,第4和第5个地址作为地址列表。所以把它们粘贴到 Create 按钮的输入框里。
现在确保我们已经选了:
-
环境:JavaScript 虚拟机
-
账户:部署合约时的第一个账户(以 0xca3 开头)
-
把上面的地址数组粘贴到创建按钮的输入框里
点击 Create 按钮我们就可以看到接下来会发生的事情。
3.合约部署之后
这个合约现在已经在 JavaScript 虚拟机上部署好了,合约的地址已经显示出来了 (0x692…)。在我们的演示里我们不用这个地址。需要时这个地址可以在其他的用例中使用。
还有,public变量已经出现了,它们是:
-
shareholders 股东
-
numShareholders 股东数
-
creator 创建者
还有我们在合约中定义的两个函数:
-
shareRevenue()
-
kill()
在做其他事之前,我们观察到账户余额少了一小部分。差额(417,626 wei,1 wei = 10^(-18) ether)是部署合约的费用。在真实环境中,当你部署合约时会从你的账户中扣除真正的以太币。
4.与已部署的合约进行交互
(1)检查变量
我们首先可以通过点击变量按钮检查变量。在这里我们检查股东数量和创建者。对于股东数量,由于这是一个数组,我们需要指定数组索引(0、1或2),这与我们在部署(创建)合约时所传入的地址列表相符。
所有的变量都是我们期望的样子。
(2)执行 shareRevenue() 函数
现在我们执行 shareRevenue() 函数。在执行这个函数时,我们用第一个账户存入30以太币(这仅仅在这个函数里需要,在许多用例中它是不需要的)。根据合约逻辑,30以太币被均分到列表里的每个账户中,也就是第3、第4和第5个账户。目前,他们每个人的余额仍然是100以太币。
我们用相同的界面来执行这个函数。 在这里我们要确保:
-
在 Account 输入框,选中第一个账户(以 0xca3 开头的账户)
-
Value 设置为 30 以太币
函数执行之后,我们查看每个账户的余额,检查它是否根据我们的设计进行了执行。
首先,我们看到,第一个账户减去了 30 以太币,其他 3 个都变成了 110 以太币。 也就是说,这 30 以太币从第一个账户中减去,然后平均分配到了其他三个账户。 这完全是根据合约执行的。
还有,如果我们仔细检查第一个账户的余额,它还减去了另一小笔以太币。差额是47,776 wei,这是交易费。每一个交易、函数的执行,或者合约部署都会花费一小笔以太币。
5.交易日志
在我们的例子中并没有接触到交易日志,不过所有操作都保存在了日志里,即使是对变量的查询也保存了。让我们了解下两个特定日志的细节。
(1)合约部署
我们可以看到是谁部署了这个合约,这个合约的地址,以及部署合约的花费。
(2)执行 shareRevenue() 函数
我们再一次看到了交易的花费。在 shareRevenue() 函数中,有一个 boolean 类型的返回值,在解码输出里我们看到这是一个真值。还有,我们在 “logs” 中看到了一个标识分发成功的事件(event)。
6.总结
这就是 Remix 如何帮助我们测试代码的。它有非常方便的功能和直观的用户界面。 在下一篇文章中,我们将使用另一个环境,testrpc,来处理同一个合约,并了解它是如何工作的。
智能合约开发的最佳工具第二部分:TestRPC上的web3
概览
TestRPC 是以太坊区块链的一种模拟,它带有 10组 预定义的以太坊帐户和支持助记符(即,你可以用相同的助记符集合生成相同的帐户集合)。它不像 Remix,它没有用户界面,我们需要用到节点控制台加上 web3 库,来与区块链进行交互。
准备
演示是通过命令行或终端来完成的,需要一个支持分屏的终端工具。在我的 Mac 上我用的是 iTerm2.
安装节点和npm:在你的平台上安装时请参考这里。
备注:我最近发现用 npm 安装web3时,安装的是 1.0.0 beta 版,在之前版本(基于 0.20.4 的版本)上的命令不能用。因此,我们改为指定的 web3 版本。
下面所有的命令都是0.20.0版本的。
打开一个终端,把屏幕分成两部分。左边是节点控制台,我们大多时间都是在这个窗口进行的。右边窗口我们运行TestRPC。
启动TestRPC
在右边的窗口,启动TestRPC
我们观察到:
-
TestRPC 是一个在内存中模拟以太坊区块链的节点程序。
-
预定义了10个账户。
-
这些账户是通过助记符生成的,每次启动TestRPC时这些账户都不同。为了使用相同的账户,我们在运行 TestRPC 时,可以使用上面提到的助记符作为参数。
-
还有,RPC会在本机(localhost)端口 8545 上打开。Web3 通过此端口访问区块链。
假设这个以太坊区块链上一切工作正常,那我们就不会过多接触这部分。现在我们把精力放到节点控制台上(左边)。在测试中,我们可以在 TestRPC 的一侧持续地看到具体的命令和发布到区块链上的日志。
Web3对象
我们需要告知节点控制台我们正在使用的是web3,并指定区块链web3正在使用的接口。
这就是 TestRPC 中创建的账户。
一个显示余额的便利的函数
我发现了一个非常便利的函数(链接在这里),可以显示所有账户的余额。就是这个函数。
简单地把这个函数复制粘贴到节点控制台里就可以了。现在我们能随时调用这个函数 checkAllBalance() ,而且它会把账户余额以以太币为单位显示出来。注意,在我们退出节点控制台之后这个函数就不能用了,但我们随时都可以把它加进去。
合约部署
1.合约编译
现在所有准备工作都做好了。我们可以部署收益共享合约了。
我们需要重新打开 Remix,因为我们要沿用 Remix 上的编译器。我们把合约代码粘贴到 Remix 上之后,它就自动编译了。这里我们要用到合约部署的结果。
点击 Detail 和 Compile 标签,那里有很多信息。
在这些信息中,我们对其中的三个比较感兴趣,字节码、ABI(应用二进制接口)和Web3Deploy
字节码是合约编译后的二进制版本,是能够在以太坊虚拟机上运行的指令集,ABI(应用二进制接口)是我们与合约字节码进行交互的接口。
Remix 已经足够友好,它能够为我们准备用于通过 web3 来进行部署的代码,字节码和ABI已经包含在了相应命令中。因此,我们只需要 Web3Deploy 这部分。
2.合约部署
首先,正如合约要求的,我们需要定义一组目标账户。出于演示目的,这三个账户是从第二个账户开始的,即:从 eth.accounts[1] 到 eth.accounts[3] 。
然后,我们按照Web3Deploy建议的方式继续进行。
基于 ABI 为收益共享合约创建一个类。直接从 Web3Deploy 中复制这一行就可以了。
node console > var revenuesharingContract = web3.eth.contract([{"constant":true,"inputs":[],"name":"creator","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"kill","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"numShareholders","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"","type":"uint256"}],"name":"shareholders","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"shareRevenue","outputs":[{"name":"success","type":"bool"}],"payable":true,"stateMutability":"payable","type":"function"},{"inputs":[{"name":"addresses","type":"address[]"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":false,"name":"_amount","type":"uint256"},{"indexed":false,"name":"_numShareholders","type":"uint256"}],"name":"Disburse","type":"event"}]);
现在就可以用字节码和其他必要信息部署合约了。再次强调,我们可以从Web3Deploy中复制这一行。部署后的合约是一个被命名为 revenuesharing 的对象。
node console > var revenuesharing = revenuesharingContract.new( addresses, { from: web3.eth.accounts[0], data: '0x6060604052341561000f57600080fd5b60405161049d38038061049d833981016040528080518201919050506000336000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055508151600281905550600090505b81518110156100f957818181518110151561009157fe5b906020019060200201516001600083815260200190815260200160002060006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff160217905550808060010191505061007a565b50506103938061010a6000396000f30060606040526004361061006d576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806302d05d3f1461007257806341c0e1b5146100c757806368eca613146100dc578063ab377daa14610105578063e579a0bd14610168575b600080fd5b341561007d57600080fd5b61008561018a565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b34156100d257600080fd5b6100da6101af565b005b34156100e757600080fd5b6100ef610240565b6040518082815260200191505060405180910390f35b341561011057600080fd5b6101266004808035906020019091905050610246565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b610170610279565b604051808215151515815260200191505060405180910390f35b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16141561023e576000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16ff5b565b60025481565b60016020528060005260406000206000915054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b60008060006002543481151561028b57fe5b049150600090505b60025481101561031d576001600082815260200190815260200160002060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff166108fc839081150290604051600060405180830381858888f19350505050151561031057600080fd5b8080600101915050610293565b7f9c26340b8d01b4e039192edfd25f4a56ed070d45afe866b8685658b1ed3cd74d34600254604051808381526020018281526020019250505060405180910390a1600192505050905600a165627a7a72305820f0e717ba935e00c43896cc9266a85af91a519061c044503be0a52b93f721d1610029', gas: '4700000' }, function (e, contract){ console.log(e, contract); if (typeof contract.address !== 'undefined') { console.log('Contract mined! address: ' + contract.address + ' transactionHash: ' + contract.transactionHash); } })
我们会看到合约已经(几乎是立即)被挖出了(也就是被记录到区块链上了)。
我们现在就可以用 revenuesharing 对象和这个部署的合约进行交互了。
与部署的合约交互
部署后的合约是通过 revenuesharing 对象进行访问的。
1.检查public变量
我们可以检查那些被标记为 “public” 的变量。
2.执行 shareRevenue() 函数
在我们执行 shareRevenue() 函数之前,我们先看一下余额。
注意,accounts[0] 的余额减少了一点,accounts[0] 是部署合约的账户。部署合约的费用是 417,626wei。你可以检查这个费用,它确实就是先前在 Remix 中部署合约时所花费的交易费。
现在我们来执行这个函数。
node console > revenuesharing.shareRevenue({from: web3.eth.accounts[0], value: web3.toWei(30), gas: 4700000});
在这里,我们正在调用 shareRevenue() 函数,以30以太币作为输入(toWei 是一个 web3 的函数,用于把 30 以太币转化成 wei),并且指定它由 account[0] 来执行。我们还加入了我们允许在这次调用中花费的 gas 值(它显然超过了需求,但执行后我们会得退款)。
交易执行后,我们可以再次查看余额。
我们做到了我们希望做的:从账户 0 减去 30 以太币,并把它们平均分配给账户1~3(现在账户1~3都是110以太币了)。而且,为了执行这个交易支付了一小笔以太币。又是 47,776 wei,与我们之前在 Remix 中看到的相符。
总结
我们在 TestRPC 上做到了同样的事情。除了我们必须在节点控制台和 web3 上与 TestRPC 的区块链进行交互以外,整个流程几乎和在 Remix 中完全相同。下一次,我们将在私有以太坊区块链上做几乎相同的事情。