使用Solidity的智能合约入门

136 阅读12分钟

在这篇文章中,我想让读者了解建立DApp(去中心化应用程序)是什么样子的,纯粹从技术生态系统和运行一些东西所需的代码。

让我们开始吧!

合同

首先,我想概述一下我们要建立的应用程序。如果我想到区块链技术,我脑海中总是浮现出与金钱信任有关的东西,以及炒作热门词汇,所以为鲁莽的初创公司建立一个疯狂的众筹系统似乎是很自然的!;)

我提出 "赢家通吃 "众筹合同

  • 创建合同时要注意
    • 项目提案的最低参赛费(为避免垃圾邮件)
    • 项目提案截止日期
    • 活动截止日期
  • 在项目提案截止日期之前,人们可以输入他们的项目,提供一个名称、一个网址参赛费
  • 当项目提案截止日期结束时,人们可以用乙醚(金钱)投票给他们想支持的项目
  • 当活动截止日期结束后,所有认捐的钱(包括报名费)都归于得票最高的项目(收到最多的以太坊)。
    • 此后,合同就结束了

也许有点疯狂,不太实用,但绝对是一个有趣的例子 :)

设置

首先,和其他软件项目一样,我们需要一些初始设置来进行。已经有一些框架(当然有......)用于构建DApps,下面的资源中提到了这些框架。然而,在我们的例子中,我们将保持一个非常简单的设置。

我们将使用Solidity作为构建合约的语言,使用web3.js进行测试并创建一个简单的前端与合约进行交互。

因为安装依赖性很麻烦,我们也将使用Docker来运行我们的本地区块链和构建我们的合约。我们将使用testrpc作为我们的本地区块链,这很方便,因为智能合约使用Gas(金钱)来运行,而使用testRPC至少不会花费我们任何真金白银,所有区块都会被即时开采。

在这个例子中,我们不会把合约部署到任何真正的区块链上,但是你可以在Ethereum和Solidity的官方文档中找到很多关于这个主题的文档。

现在,对于编译我们的智能合约,有几种选择。我们可以使用浏览器-Solidity作为Web-IDE,并在每次修改时从那里复制/粘贴生成的web3 代码。然而,我更喜欢在本地工作。

为了做到这一点,我们需要solc 来编译我们的合同,然后one-lineify (删除换行符)合同,并以某种方式将其纳入我们的JavaScript应用程序web3.js 。为此,我创建了一个docker容器,它挂载给定的文件夹,在该文件夹的contract.sol 文件上运行solc ,然后one-lineify's代码并将其写入同一文件夹的contract.js 文件中。

现在,这对这个例子来说是可行的,但不能很好地概括(例如:多个合同),但下面提到的框架都有建立合同的机制。

我们也可以走不同的路线,将solc 的二进制输出自动粘贴到JavaScript文件中,或者以任何其他方式将我们编译的合约放入我们的应用程序和区块链上。但是对于这个非常简单的例子,这种基于容器的简单方法就足够了。

要在本地运行 testRPC,执行

docker pull harshjv/testrpc
docker run -d -p 8545:8545 harshjv/testrpc

要运行 solidity 构建容器,执行

docker pull mzupzup/soliditybuilder
docker run -v /path/to/this/folder:/sol mzupzup/soliditybuilder

或者,如果你也想自动观察文件(在Windows上不行),你也可以使用。

docker pull mzupzup/soliditywatcher
docker run -v /path/to/this/folder:/sol mzupzup/soliditywatcher

如果这一切都成功了,我们就可以开始研究我们的合同了

合同的执行

这个应用程序的核心部分将是合同。完整的代码可以在GitHub repo中找到contract.sol

我发现使用浏览器-solidity来开发合同的基础是非常方便的,然后,在我有了一个想法之后,再转移到我的本地设置和web3.js 测试(如下所述)。

我将一步一步地查看代码,并解释我所做决定背后的原因。请记住,Solidity 正处于非常繁忙的开发阶段。从我写这个例子到现在,Solidity0.4.60.4.10 ,有很多变化,所以当你读到这个例子时,它可能已经过时了;)

另外,由于这是我的第一个合约,而且整个智能合约领域相对较新,还没有建立很多最佳实践,所以我相信很多事情可以比我做得更有效/更优雅。

我采取了一个简单的方法,当我遇到一个问题,在文档中找不到答案或快速的网络搜索,然后我建立了一个变通方案。其中一些变通方法可能看起来很奇怪,但如果你考虑到这些合同需要在一个去中心化、无信任的系统上运行,语言中的一些限制就会变得更加清晰。

pragma solidity ^0.4.6;

契约的第一行告诉编译器下面的代码是用哪个版本写的。

contract WinnerTakesAll {

之后,我们定义我们的合约--这与其他语言中的class 语句相似。

    uint minimumEntryFee;
    uint public deadlineProjects;
    uint public deadlineCampaign;
    uint public winningFunds;
    address public winningAddress;

我们为我们的合同定义了一些状态变量,在这里我们将保存初始参数,哪个项目处于领先地位,以及它已经承诺了多少钱。其中一些有public 修改器,这意味着,我们可以从外部访问它们(只读)。这样,我们就可以在我们的用户界面中显示最后期限和当前的赢家。

    struct Project {
        address addr;
        string name;
        string url;
        uint funds;
        bool initialized;
    }

Solidity 浏览器为我们提供了创建用户定义的结构的能力,称为 。虽然我们只能structs我们的合同内部使用这些结构,但它们对于组织我们的数据仍然是有用的。在这种情况下,我们为一个提议的 的结构。Project

    mapping (address => Project) projects;
    address[] public projectAddresses;
    uint public numberOfProjects;

为了跟踪我们合同内的状态变化(例如:用户向项目认捐),我们还创建了一个所谓的mapping 。这些映射类似于其他语言中的hashMaps ,但有一些限制(例如不能迭代)。

Solidity 不允许我们将这些映射暴露给公众,所以我们也至少在一个address[] 数组中跟踪被提议的项目的地址,以便能够从用户界面查询被提议的项目的信息。另一个限制是,这些数组,即使是公开的,查询起来也有点麻烦,所以我们也保存numberOfProjects 变量,以便我们知道在任何时间点有多少项目被提议。

    event ProjectSubmitted(address addr, string name, string url, bool initialized);
    event ProjectSupported(address addr, uint amount);
    event PayedOutTo(address addr, uint winningFunds);

这些event,在我们的合同中只用于调试的目的。我们基本上可以用它们来记录到区块链上,这在测试和调试合约时是有帮助的。我们也可以使用web3.js 来监听这些事件,这将在下一节描述。

在这种情况下,我们为我们所有的交易以及合约的完成创建一个事件。

    function WinnerTakesAll(uint _minimumEntryFee, uint _durationProjects, uint _durationCampaign) public {
        if (_durationCampaign <= _durationProjects) {
            throw;
        }
        minimumEntryFee = _minimumEntryFee;
        deadlineProjects = now + _durationProjects* 1 seconds;
        deadlineCampaign = now + _durationCampaign * 1 seconds;
        winningAddress = msg.sender;
        winningFunds = 0;
    }

现在,我们已经完成了所有的状态变量和事件,我们可以开始使用我们的函数了

这个函数是constructor ,它是创建合同的交易。在这种情况下,我们提供了两个deadlines ,以及我们想要设置的minimum entry fee

正如你所看到的,Solidity 提供了一个很好的API,可以用now 关键字处理日期,以及可以从中增加和减少时间单位。

如果活动截止日期在提案截止日期之前,我们throw ,这基本上是Solidity的一种抛出错误的方式。这意味着,整个交易在此时被取消,所有资金被退回。

    function submitProject(string name, string url) payable public returns (bool success) {
        if (msg.value < minimumEntryFee) {
            throw;
        }
        if (now > deadlineProjects) {
            throw;
        }
        if (!projects[msg.sender].initialized) {
            projects[msg.sender] = Project(msg.sender, name, url, 0, true);

            projectAddresses.push(msg.sender);
            numberOfProjects = projectAddresses.length;
            ProjectSubmitted(msg.sender, name, url, projects[msg.sender].initialized);
            return true;
        }
        return false;
    }

好了,有了我们构建的合同,现在是时候提交我们的第一个项目提案了。我们用submitProject 函数来做这件事,它需要一个项目的名称和一个URL。

注意到payable 修饰符的使用,这意味着这实际上是一个transaction 。这意味着,我们可以访问msg.valuemsg.sender 变量,这些变量是交易发送者的地址和他们发送的资金。

我们检查msg.value 是否高于我们指定的最低入会费,如果不是,我们throw ,取消该交易。我们还throw ,如果提议的截止日期在过去。

然后我们检查,是否已经有一个来自发送地址、带有initialized 标志的项目。不幸的是,没有办法检查一个mapping 是否已经包括一个密钥,因为它被自动初始化为每个可能的密钥,所以我们必须这样做。

如果项目来自一个新的地址,我们创建一个Project ,把它添加到mapping 以及我们的address[] 列表中,并触发我们的ProjectSubmitted 事件。

    function supportProject(address addr) payable public returns (bool success) {
        if (msg.value <= 0) {
            throw;
        }
        if (now > deadlineCampaign || now <= deadlineProjects) {
            throw;
        }
        if (!projects[addr].initialized) {
            throw;
        }
        projects[addr].funds += msg.value;
        if (projects[addr].funds > winningFunds) {
            winningAddress = addr;
            winningFunds = projects[addr].funds;
        }
        ProjectSupported(addr, msg.value);
        return true;
    }

现在我们有了项目,我们可以向它们认捐一些钱。也就是说,如果提案期限已过,而我们仍在活动截止日期之前。

这个函数也是payable ,所以我们又可以访问msg.value ,也就是认捐给项目的金额。我们检查截止日期,检查认捐值是否为正数,甚至检查是否有一个给定地址的项目。如果其中任何一项失败,我们throw ,并取消该交易。

如果一切顺利,我们增加项目的资金并更新获胜的项目,同时触发ProjectSupported 事件进行调试。

    function getProjectInfo(address addr) public constant returns (string name, string url, uint funds) {
        var project = projects[addr];
        if (!project.initialized) {
            throw;
        }
        return (project.name, project.url, project.funds);
    }

现在,为了使我们的用户界面能够显示可用的项目,我们需要这样做,因为如果用户不能得到任何关于项目的信息或寄钱的地址,他们将很难支持一个项目,我们还提供了一个getProjectInfo 功能。

请记住,正如我上面所说,我们还不能返回struct,但我们可以做的是从一个函数中返回多个值,这正是我们在这里做的。

同时注意到constant 修改器,这意味着这个函数不会修改区块链的状态,因此不需要花费任何Gas。

    function finish() {
        if (now >= deadlineCampaign) {
            PayedOutTo(winningAddress, winningFunds);
            selfdestruct(winningAddress);
        }
    }
}

好吧。假设人们提出了一些项目,我们也有一些人向这些项目认捐了资金。然后会发生什么?最理想的情况是,一旦活动期限结束,我们会自动结束合同。

然而,这并不容易做到,我们必须根据一些时间来触发它,像一个cronjob。现在,有一些解决方案,例如这个,但在我们的案例中,我们将只是创建一个finish 函数,它必须在截止日期结束后的某个时间点被调用(例如由赢家)。

如果截止日期没有结束,我们就什么都不做。然而,如果它是,我们基本上只是触发一个event ,并通过winningAddress 调用selfdestruct

这个selfdestruct 是一个内置函数,它关闭了合同,并将其中所有的钱支付给给定地址。因此,我们的 "赢家通吃 "众筹就这样结束了。

就这样了!现在,我们如何测试这个?

调试/测试

调试和测试智能合约并不特别容易,但这是可能的。下面提到的框架都包括一些单元测试智能合约的方法,例如,只需启动testRPC ,用web3 编写异步JavaScript测试(例如:chai /mocha ),并验证它们对区块链有一些影响。还有一些有用的工具,如Chaithereum

我在上面提到Events ,作为调试Solidity合约的机制,web3 有一种方法可以监听这些事件,这在编写复杂的合约逻辑时有很大帮助。

你可以使用allEvents 来做这个。

var events = myContractInstance.allEvents([additionalFilterObject,] function(err, log){
  if (!err)
    console.log(log);
});

在这种情况下,我们只记录事件,但你可以想象,我们也可以利用这个来实现使用事件的记录功能。

一般来说,web3 ,因为它是一个完整的区块链API,可以很好地帮助。

查询合约的公共接口(获取器/公共变量)。

crowdfunder.numberOfProjects(function(err, data) {
    // handle error and data
});

crowdfunder.projectAddresses[0];

向合约发送交易

crowdfunder.submitProject.sendTransaction(projectName, projectURL, {
    from: senderAddress,
    value: entryFee,
    gas: 600000,
}, function(err, data) {
    // handle errors and data
});

检查区块链上不同账户的余额

web3.eth.getBalance(web3.eth.accounts[0]);

web3.eth.getBalance(web3.eth.accounts[1]);

以及转换到/从Wei和其他一些实用工具。

web3.fromWei(web3.toDecimal(web3.eth.getBalance(web3.eth.accounts[0])), 'ether');

crowdfunder.minimumEntryFee(function(err, data) {
    if (!err && data) {
        console.log(web3.fromWei(web3.toDecimal(data), 'ether') + ' ether'));
    }
});

总而言之,web3 是相当好的文档,并且像人们所期望的那样工作,提供了一个丰富的接口来构建和测试DApps。

前端实现

我还快速地拼凑了一个简单的Web-UI,以便以更好的方式测试该应用程序。这个UI的代码(不是很美观,也没有经过修饰)也在[GitHub的回放]中。

它使用

  • [async.js]
  • [web3.js]
  • [基础]

该应用程序连接到testRPC ,编译合同并提供处理程序和用户界面。

  • 创建合同
  • 提交项目建议
  • 为一个项目认捐乙醚
  • 完成合同

主要使用web3.js 和上述调试/测试中提到的方法。

这就是它的模样:

image.png

结论

我的意思是,当然,在后端它是非常不同的。它不是一个GoNodeJS 应用程序,而是一个运行在以太坊区块链上的智能合约,你有一些新的交易(部署的气体成本/状态改变的操作)。

但从一个前端开发者的角度来看,它实际上与通常的Web开发非常相似。你有一些异步调用的端点来获取数据和进行交易,所有这些都被打包在一个(希望)设计良好的单页应用程序中。

此外,由于以太坊生态系统似乎更倾向于JavaScript(web3 ,在Solidity ),JavaScript开发人员将能够使用许多他们已经知道和喜爱的东西。