首页 > 其他分享 >精通以太坊(全)

精通以太坊(全)

时间:2024-05-01 15:59:16浏览次数:8  
标签:精通 函数 以太 创建 区块 合约 我们

原文:zh.annas-archive.org/md5/119107c4466aa665f9e9ebea52f51e20

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

以太坊是构建区块链应用程序最常用的平台之一。它是一个去中心化的应用平台,可以完全按照程序设计运行,而不受欺诈、审查或第三方干扰的影响。

这本书将帮助您深入了解区块链的工作原理,以便您可以发现整个生态系统、核心组件和实现。您将首先学习如何配置和使用各种以太坊协议来开发 dApps。接下来,您将学习如何使用 Solidity 和 Vyper 编写并创建可扩展的强大智能合约。然后,您将探索 dApps 架构的构建块,并通过多个现实世界的例子了解创建自己的 dApp 的见解。本书甚至将解释如何按照所需的最佳实践和技术在多个以太坊实例上部署您的应用程序。接下来的几章将深入探讨构建高级智能合约和使用以太坊构建多页面前端的高级主题。您还将学习如何实现机器学习技术来构建去中心化自治应用程序,以及跨各种领域(如社交媒体和电子商务)的多种用例。

在本书结束时,您将拥有足够的专业知识,可以自信地构建去中心化自治应用程序。

这本书是为谁准备的

这本书适用于任何想要构建快速、高度安全和可交易的去中心化应用程序的人。如果您是一名以太坊开发者,希望完善您在构建强大的区块链应用程序方面的现有技能,那么这本书适合您。了解以太坊和区块链的基本知识是理解本书涵盖概念的必要条件。

本书涵盖了什么

第一章,区块链架构,涵盖了在开始之前必须了解的基本区块链知识,以确保我们理解本书中将要使用的要求。我们将从一开始就涵盖以太坊的历史,最终涉及更复杂的开发主题。

第二章,以太坊生态系统,向我们展示了区块链的基础设施和设置。我们将深入分析区块链的工作原理,以便了解整个生态系统。我们还将学习有关以太坊组件及其各种实现的信息。

第三章,以太坊资产,向我们展示了在开发其中的应用程序之前如何配置和使用以太坊协议的各种实现。有几种不同的版本,可以在开发、测试和部署中互换使用。稍后,我们将研究修改后的 Ghost 实现、计算和图灵完备性,以更好地理解以太坊的工作流程。了解并使用以太坊生态系统中的工具和服务也非常重要。

第四章,精通智能合约,教导我们超越普通用例的高级智能合约。它是有用的,因为它将帮助我们理解改进我们的 Solidity 和 Vyper 编码技能的高级技术、方法和工具,从而创建可扩展的强大智能合约。

第五章,精通 dApps,向你介绍了去中心化应用程序(dApp)架构及其构建模块。然后,我们将逐步从头开始创建一个 dApp,从空白应用程序到编写智能合约、创建加密钱包并对其进行单元测试。我们将向您展示如何为这些应用程序创建专门的 UI。我们还将向您展示如何使用其他测试服务,例如 Truffle,来构建无缝的 dApps。然后,我们将向您展示如何在多个以太坊实例上部署您的应用程序,例如 testrpc、私有链、测试链和主网络。

第六章,工具、框架、组件和服务,向我们介绍了几种重要的工具,供以太坊开发人员创建功能强大的 dApps 和智能合约,这些智能合约能够扩展并且足够安全,能够为数百万潜在用户提供服务。

第七章,在测试网络上部署,向我们展示了如何在测试网络上部署我们的区块链应用。以太坊区块链应用可以部署在 Rinkeby-testnet 上。测试网络提供了测试汇集,开发人员可以在其中测试所有智能合约和 dApps 的行为。Gas 计算和其他因素可以在测试网络上进行测试。

第八章,各种 dApps 集成,向前迈进,并向您展示如何使用工作流创建更高效的 dApps,这将帮助我们更快速、更安全地开发代码。我们将了解与去中心化 web 应用程序开发相关的所有方面,包括使用 React 进行 web 客户端代码编写;使用 Node.js、Solidity 或 Vyper 进行后端代码编写;以及使用 web3.js 进行 dApp 通信。

第九章,去中心化交易所工作流,向你展示了如何通过轻松易懂的语言理解这些复杂系统的复杂性,从而创建复杂的去中心化交易所DAXs)的章节,详细涵盖了每个部分。

第十章,以太坊区块链上的机器学习,利用区块链中的智能合约自动验证解决方案,因此不会有关于解决方案是否正确的争论。提交解决方案的用户不会有对手方风险,即他们不会因为工作而没有得到报酬。合约可以由任何具有数据集的人轻松创建,甚至可以通过软件代理程序以编程方式创建。

第十一章,创建基于区块链的社交媒体平台,详细介绍了创建一个坚实的去中心化社交媒体平台所需的步骤,该平台使用 Swarm 进行资源的分布式存储,比如视频,并利用区块链的无信任特性来保证每个个体的隐私。

第十二章,创建基于区块链的电子商务市场,介绍了搭建一个用于购买、出售、租赁和交换商品的去中心化市场所需的步骤,类似于 OpenBazaar。我们将构建一个以可扩展性为重点的独特市场。

第十三章,创建去中心化银行和借贷平台,创建了一个使用以太坊管理和存储加密货币的去中心化银行。我们将使用冷存储以增加安全性,并设置具有抵押违约保护的借贷系统。这是一个在线章节,可以在以下链接找到:www.packtpub.com/sites/default/files/downloads/Creating_a_Decentralized_Bank_and_Lending_Platform.pdf

获取本书的最大收益

您需要对区块链和以太坊有基本了解。

下载示例代码文件

您可以从您在www.packt.com的帐户中下载本书的示例代码文件。如果您在其他地方购买了这本书,请访问www.packt.com/support并注册,以使文件直接通过电子邮件发送给您。

您可以按照以下步骤下载代码文件:

  1. 登录或注册www.packt.com

  2. 选择“支持”标签。

  3. 点击“代码下载和勘误”。

  4. 在搜索框中输入书名,并按照屏幕上的指示操作。

下载文件后,请务必使用最新版本的软件解压或提取文件夹:

  • Windows 可使用 WinRAR/7-Zip

  • Mac 可使用 Zipeg/iZip/UnRarX

  • Linux 可使用 7-Zip/PeaZip

本书的代码包也托管在 GitHub 上,链接为github.com/PacktPublishing/Mastering-Ethereum。如果代码有更新,将在现有的 GitHub 存储库上进行更新。

我们还有其他代码包,来自我们丰富的图书和视频目录,可在github.com/PacktPublishing/上找到。去看看吧!

下载彩色图片

我们还提供一本 PDF 文件,其中包含本书中使用的屏幕截图/图表的彩色图片。您可以在这里下载:www.packtpub.com/sites/default/files/downloads/9781789531374_ColorImages.pdf

使用的约定

本书中使用了许多文本约定。

CodeInText:表示文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟网址、用户输入和 Twitter 用户名。以下是一个示例:“他们使用一个名为0x.js的库,它允许您使用高级、清晰的界面与转发器进行交互,使用起来非常棒。”

代码块设置如下:

pragma solidity 0.5.0;
contract Example {
    uint256 myStateVariable;
    string myOtherStateVariable;
    function example(){
        uint256 thisIsNotAStateVariable;
    }
}

当我们希望引起您对代码块的特定部分的注意时,相关行或项目会以粗体显示:

function example(string memory myText) public {
    require(bytes(myText)[0] != 0);
}

任何命令行输入或输出都以以下形式编写:

$ git clone https://github.com/merlox/dapp

粗体:表示一个新术语、一个重要词或屏幕上看到的词。例如,菜单中的字词或对话框中的字词会在文本中显示为这样。以下是一个示例:“转到 Droplets 部分,然后单击创建 Droplet。”

警告或重要说明会显示为这样。

贴士和技巧显示为这样。

联系方式

我们一直欢迎读者的反馈。

一般反馈:如果您对本书的任何方面有疑问,请在您的消息主题中提及书名,并通过[email protected]发送电子邮件给我们。

勘误:尽管我们已经尽一切努力确保内容的准确性,但错误是难免的。如果您在本书中发现错误,请将其报告给我们,我们将不胜感激。请访问www.packt.com/submit-errata,选择您的书籍,点击勘误提交表格链接,并输入详细信息。

盗版:如果您在互联网上发现我们作品的任何形式的非法拷贝,我们将不胜感激地请您提供位置地址或网站名称。请通过[email protected]联系我们,并附上材料链接。

如果您对成为作者感兴趣:如果您对某个您擅长的主题感兴趣,并且有意要么撰写书籍,要么为书籍作出贡献,请访问authors.packtpub.com

评论

请留下评论。一旦您阅读并使用了本书,为什么不在购买书籍的网站上留下评论呢?潜在读者可以看到并使用您公正的意见来做出购买决定,我们在 Packt 可以了解您对我们产品的看法,而我们的作者可以看到您对他们书籍的反馈。谢谢!

有关 Packt 的更多信息,请访问packt.com

第一部分:区块链-以太坊复习

在本节中,您将深入了解如何更好地编写智能合约和 dApp,并审查假设以改善代码背后的心理。

以下章节包括在本节中:

  • 第一章,区块链架构

  • 第二章,以太坊生态系统

  • 第三章,以太坊资产

第一章:区块链架构

当涉及理解区块链的内部工作时,区块链架构涵盖了基本原理。能够在使用不同领域的以太坊的不同项目上工作是至关重要的,因为一旦你对一切是如何协同工作有了扎实的了解,你的思维就会开始以不同的方式看待事物。当你使用区块链并为其编程时,你将会对区块链中发生的事情有一个高层次的概览。一旦你阅读了本章,这个复杂生态系统的移动部分将变得非常清晰,因为你将会对智能合约的工作原理和它们与底层结构的关系有一个高层次的概览。

在本章中,我们将涵盖以下主题:

  • 超越以太坊

  • EEA

  • 理解以太坊区块链

  • 智能合约工作原理的高层概览

  • 重要的智能合约编程

超越以太坊

你可能已经了解了以太坊是什么,但为了确保我们在同一个页面上,重要的是你对以太坊的真实本质有一些背景知识,以便在进一步学习时不会分散太多注意力。

解释以太坊

以太坊首先是一个区块链。以太坊是一项在许多计算机上运行的技术,它向其消费者提供了一个保证,即他们信任的是一个可靠的系统,该系统将按预期工作。

“以太坊是世界计算机。”

-维塔利克·布特林

要了解更多关于核心以太坊理念的内容,请查看他们的官方网站:ethereum.org

连接在世界各地的数千台计算机形成的网络被称为节点,它们允许其他人获取他们所需的信息,同时信任代码以实现去中心化的互联网。

为什么去中心化对互联网如此重要?因为我们已经到了一个时候,少数几家大公司控制着你和我可以生产或消费的信息。

政府有太多的权力,以至于他们正在失控地制定他们的规则。他们偏向于有利于他们和他们的统治者的事物。这是可以理解的——每当某个实体处于食物链的顶端时,迟早他们会控制位于它下面的整个系统。

以太坊的目标是创建一个抗审查和开放的平台,使人们能够信任智能合约,这些合约强制执行的规则不能被第三方实体控制。

当你发布一个智能合约时,你有 100%的保证,即代码将在任何时候运行,除非它的规则规定了其他情况。

以太坊的历史

以太坊在 Vitalik Buterin 的 2013 年白皮书中进行了描述,可以在此处找到:github.com/ethereum/wiki/wiki/White-Paper。他谈到了需要一种脚本语言,可以在比特币之上运行,因为他参与了比特币杂志,并且他了解比特币区块链的限制。

他看到了创造一个运行在去中心化技术上的平台的机会,以创建新类型的应用程序。

不多的人相信他的愿景,所以他决定自己和一个小团队创建一个全新的区块链,他们看到了 Vitalik 的想法的潜力。他成立了以太坊瑞士集团,并决定在 2014 年 7 月举行一次首次代币发行ICO),在那里他以以太币换取比特币,共筹集了约 1800 万美元。

他创建了智能合约技术,基本上是无需信任实体执行的程序。它们始终可用,并且无故障运行。

以太坊提供了一个允许人们在区块链上创建自己应用程序的系统,这是它成功的原因。在以太坊之前,没有一种简单的方法在去中心化平台上创建去中心化应用程序dApps)。比特币有一个使用操作码的协议,使用一种名为 Script 的编程语言创建简单的应用程序,但它的能力有限,因为它是非常低级的,并且受到区块大小的限制。

以太坊的发展

以太坊的发展计划分为四个不同阶段,每个阶段都有重大变化:

  • Frontier

  • Homestead

  • 大都会

  • 宁静

它被用来在需要时交付和研究创新解决方案,并通过硬分叉实现了不向后兼容的功能。2015 年,Frontier 作为以太坊的第一个版本推出。一年后,推出了 Homestead,其中包括许多改进,使以太坊成为一个功能强大、能够处理智能合约的系统。

在以太坊之上进行的最大的 ICO 之一是去中心化自治组织 ICO,也被称为DAO,它筹集了来自 11000 多人的 1.5 亿美元。问题在于,它被一个未知的个人组织攻击了,他们将资金转移到了另一个 DAO。有趣的是,一个名为白帽子组织的程序员团队看到了这次黑客攻击正在发生,并尽可能多地将资金转移到了一个名为白帽子 DAO的独立去中心化组织中,他们在那里存放着人们的资金以供以后分配。

这一事件引发了社区内的激烈争论,导致以太坊分为两个派别,一些人认为以太坊必须是不可变的,不应该被修改,而另一些人则认为应该进行硬分叉以恢复所造成的损失。

那就是以太坊经典和我们所知的以太坊的开端。以太坊经典的用户群明显较小,但它保留了他们认为区块链必不可少的初始不可变性理念。 2017 年 3 月,几家公司联合努力创建了目前超过 500 名成员的非营利组织以太坊企业联盟EEA),其目标如下:

"创建基于开源和标准的区块链规范。"

-以太坊企业联盟

换句话说,他们创建了一个合作解决未来区块链技术的人群,以使其更好、更快和更有能力。

它遭受了几次黑客攻击,数百万美元被盗。他们不得不进行硬分叉来拯救人们的资金,并且价格的波动性非常大,但未来看起来光明,随着需求的增长而不断改进。

EEA

EEA (entethalliance.org)是由以太坊核心团队开发的最令人振奋的项目之一,因为他们打算帮助来自世界各地的公司从去中心化技术中受益。通过了解这个项目,当您成为 EEA 专家时,您将处于良好的位置。

在本节中,我们将介绍以下主题:

  • EEA 的愿景

  • EEA 的会员资格

  • EEA 架构

Vitalik 资助了组织,因为他收到了来自高管的巨大需求,希望创建可以在大公司中使用来处理要求严格的 dApps 的软件。这些公司现在希望建立以太坊的私有版本,以满足其研发需求。

这个倡议的有趣之处在于,他们与数百家公司合作研究和开发解决方案,并将其共享。例如,如果 EEA 的公司成员为更好、更快的 dApps 创建了新的实施协议,他们将与其他成员分享,以便他们也能从这一最尖端的研究中受益,同时共同发展以太坊生态系统。

EEA 的愿景

EEA 在较长期内设想实现的四个重要公共目标如下:

  • 成为一个开源标准,而不是产品

他们只与可以公开分享给任何人而没有限制的开源代码合作,以便传播可能有助于改进他们的区块链产品的开发进展。你看,他们是一个非营利组织,希望通过结合许多对私有区块链解决方案感兴趣的公司的努力,推动区块链(我们知道的)进一步发展。

  • 解决企业开发需求

EEA 帮助公司免费整合其他人发现的新创新,以便享受最新需求的好处。

  • 在公共和私有以太坊之间分享改进建议

他们希望通过从公共区块链获取改进来改善他们正在构建的东西,以便在保持一个伟大的产品的同时更快地发展。

  • 利用现有标准

当涉及到区块链技术时,还有很多需要研究和发现的地方。许多关于可伸缩性、安全性和透明度的问题正在研究中,因为这种去中心化的结构对现代计算来说是新的。因此,想法是借鉴现有标准,例如权益证明PoS),以便比其他人更快地提升。

本质上,他们试图满足许多公司对私有企业和快速区块链为其个人应用提出的要求。

EEA 成员资格

有趣的是,任何公司都可以通过在他们的网站上填写一个表格免费加入 EEA 成为该团体的成员,每年需支付一定费用。这种开放性有助于许多个人跟上以太坊区块链的新改进。

这是成为会员的年度费用的分解:

  • 不到 50 名员工:每年 3000 美元

  • 51 到 500 名员工:每年 10000 美元

  • 501 到 5000 名员工:每年 15000 美元

  • 5000 名以上的员工:每年 25000 美元

非营利组织每年只需支付 3000 美元,无论公司规模如何。通过加入联盟,你可以享受一系列普通好处,比如参与讨论、投票、开源代码、见面会,以及在你的网站上展示 EEA 徽标所带来的声望。作为一家大型区块链公司,成为成员只为了在你的网站上展示 EEA 徽标所带来的声誉是有道理的。

好处取决于你所在公司的类型,你可以在下一节中看到它们:

  • Class A 成员是联盟关联公司的董事,他们在做决定时有优先权,并且可以享受所有的好处。

  • Class B 成员是拥有一家普通公司的人,因此他们会获得标准好处而没有独家权利。

  • Class C 成员是负责 EEA 研究中的法律挑战的律师和法律公司。

  • Class D 成员是非营利组织和学术机构。他们以最低价格获得基本好处,但没有投票权。

EEA 拥有大约 500 家公司的强大联盟,其中包括英特尔、微软和摩根大通等巨头。如果你经营一家体量不小的公司,你可能会有兴趣成为 EEA 的成员,因为这意味着你将成为企业区块链革命的一部分。

EEA 架构

使用 EEA 工具构建的企业级应用具有非常有趣的结构,建立在现有以太坊结构之上。他们开发了企业以太坊架构堆栈EEAS),这是一个设计,指定了这种新类型的以太坊区块链应如何在私有环境中运作,具有专为隐私设计的功能。致力于以太坊企业项目的团队详细说明了这种私有区块链的功能,而不考虑其下层技术,如软件代码、API 和通信协议。EEA 的意图是专注于创建他们规范中描述的内容,以便人们可以为其公司享受私有区块链。

包括以下独家于企业以太坊区块链的创新解决方案:

  • 硬件安全模块HSM):这是一种安全存储数字密钥的物理计算设备,例如您的私人加密货币密钥,具有最大的安全功能。例如,Ledger 和 Trezor 是也被称为 HSM 的硬件钱包,因为它们为您的区块链私钥提供了物理设备的安全性。

  • 权限和认证:这对于以更加结构化的方式为用户赋予特定角色并限制对某些区域的访问非常有用。

  • 企业管理系统:这用于帮助公司控制其私有区块链的内部工作。

  • 预言机:这对于与构建在企业私有区块链之上的自定义智能合约进行通信的外部服务非常有帮助。它们是与外部世界交换关键信息的必要手段。

关于隐私和扩展性,我们知道许多不同的团队正在努力创建独特的解决方案,这些解决方案可能会使整个社区受益。然而,我们知道,最初,区块链将使用两个主要系统来扩展现有 dApps 的容量:

  • 与可信执行的离链交易:诸如 plasma 和状态通道等协议正在开发中,以利用可以由用户的计算机本地执行的离链交易,从而减少主区块链的计算负载。

  • 私密交易:使用零知识证明、环签名和许多其他著名协议来保证与公共区块链交换的数据的隐私的交易。这是公司需求的一个重要方面,因为它们拥有不能公开共享的私有数据和内部流程。我们无法知道他们将选择哪种解决方案,所以这取决于他们自己的决定。他们还将实现私有代码执行,使公司能够在安全环境中执行某些智能合约交易,用户将看到一个编码的哈希。

EEA 正在现有核心区块链之上构建三个额外的元素:

  • 链上私有状态:这是一个单独的存储区,用于存储智能合约的私有状态。它将为我们,开发人员,提供真正的私有变量和存储函数,我们将能够使用这些函数来保持信息安全,不被公众所见。这对企业级 dApp 非常强大。

  • 可信执行:这些系统将提供可信执行环境,在此环境中,代码将被执行,而不会干扰公共流程。

  • 私有共识:公司将能够使用自己的私有区块链为其自身系统达成协议,类似于我们可以在第三章的去中心化自治组织部分看到的投票系统。以太坊资产

最后,区块链网络层将受益于企业 P2P,该层将用于在公司设置的更小的节点网络中交换交易,以便它们享受更快的处理速度和确认时间带来的好处。企业区块链将能够与其他网络进行交互,包括公共区块链,以交换信息。

了解以太坊区块链

以太坊区块链是一个由几个重要组件组成的复杂系统,这些组件共同工作,形成一个令人印象深刻的平台,每个人都可以用来创建不可阻止的应用程序。您将学习区块链内部工作的复杂性,以更深入地了解它的构造方式。

去中心化区块链的原理

在最基本的层面上,以太坊区块链是一组连接的块,其中包含许多节点共享的信息,以便保持不可改变的数据结构,永久保存信息。任何区块链的目标都是保留信息,而不可能更改或删除它,以避免外部实体的审查或操纵。

因此,以太坊区块链通过实施几种众所周知的工具,借助比特币和先前的研究创建了运行在这些区块之上的程序,从而建立在该概念基础上。

要了解以太坊区块链的内部工作原理,我们必须了解构成区块链的每个组件。首先,我将为您概述区块链作为一组节点,然后我们将逐个从金字塔形成的较高到较低级别的每个组件进行介绍。

区块链作为一组节点

以太坊是一个去中心化平台,这意味着两个或多个节点以协调的方式共同实现用户视为单个操作的共同结果。节点根据其决定扮演的角色执行各种类型的功能。它可以使用挖矿软件提出和验证交易以达成共识,同时使用工作量证明PoW)协议来保护区块链,或者是一个轻量级节点,执行支付验证和许多其他可以使用有限资源完成的任务。

以太坊区块链基于比特币,因为由中本聪创建的系统是一个非常强大的去中心化解决方案。实际上,它使用一种系统来执行分散代码,使用能够在数十万台计算机网络上安全处理基本指令的操作码。它们必须简单以确保安全性优先。

尽管两个区块链在其系统上相当相似,但它们有明显的区别,例如以下内容:

  • 比特币和以太坊都使用 PoW 来生成区块,而以太坊打算在未来将 PoS 作为块生成系统,以避免浪费计算能力。

  • 以太坊使用 Ethash 算法,而比特币使用 SHA256 来处理交易和生成区块。

  • 以太坊的区块时间约为 15 秒,比比特币快大约 100 倍。这使人们获得更好的确认时间。

以太坊区块链是去中心化和分布式的,这意味着连接的节点在不同位置独立地彼此有几个关系,同时在不同位置运行相同的软件。这对于保证区块链不可阻止的特性非常重要。这里解释了每个术语的含义:

  • 去中心化网络:这是一个没有单一控制点的计算机网络,没有任何实体控制整个系统,每个节点都以点对点P2P)的方式直接连接到其他节点。如果区块链不是去中心化的,某个政府或实体可能会找到主控节点并立即停止整个系统。

  • 分布式网络:这是一个网络,许多不同的计算机在不同的位置运行相同的软件。如果区块链不是分布式的,某个实体可能会到所有节点所在的地方,并停止整个操作,因为它们都在同一个地方,因此没有一个是安全的免受这样的攻击。

您可以看到以下图表,说明这些类型的技术如何连接节点,以便它们可以以完全安全的方式进行通信,因为在试图理解分布式和去中心化系统之间的区别时,这通常是一个令人困惑的点:

这些特征使得以太坊网络有能力不必依赖任何人即可运行,因为成千上万的节点能够在受保护且独立的方式之间相互通信。请注意,以太坊是完全开放的区块链,这意味着任何人都可以自由加入并参与其中的决策,比如区块创建和交易处理。它允许人们以参与者的身份加入节点网络,而不需要任何要求,因为整个结构都由密码安全算法保护。

节点可以共同工作,也可以通过执行意外代码而变得恶意。那些表现出任意行为的被称为拜占庭节点。主要目标是创建一个系统,即使在最坏的情况下节点也能合作,在那里它们存在容错保护以避免破坏整个结构。但是如果做出区块链的节点可以因为是拜占庭节点而随机行事,它们怎么能一起工作呢?

最终,这是一组计算机志愿地朝着同一个目标工作。没有什么能阻止它们执行意外的动作。这正是区块链面临的最有趣的问题之一。有几种解决方案,但仍在进行研究,以在分散式系统中在性能和安全性之间获得完美平衡。

CAP 理论

CAP 理论最初由 Eric Brewer 在 1998 年提出,指出任何分布式系统不能同时具有这三个属性之一:

  • 一致性:这是节点网络在同一时间保持一致的区块链数据副本的能力。

  • 可用性:这意味着连接节点的系统在线并且始终可用,同时可以在需要时响应所有用户的请求,而无故障。

  • 分区容忍性:如果整个网络中的一组节点因任何原因停止工作或失去连接,系统不应受影响,应该继续正常工作,就好像什么都没有发生一样。

已经证明分布式或分散式系统不能同时具有这三个属性,这很有趣,因为以太坊,就像比特币一样,在看似同时实现了所有这些属性。事实上,一致性并不是与分区容忍性和可用性同时实现的,而是在稍后的时间实现的。你看,为了在这样多样化的计算机中有秩序,我们只能希望在区块链以已建立的速度增长时随着时间达到相同的数据水平。这就是称为最终一致性的概念,其中的目标是通过随时间验证多个节点来实现。因此,在比特币和以太坊中引入了挖矿的概念,以 PoW 协议达成共识。

最终,以太坊计划转向 PoS,其核心思想是,节点或用户维持一定数量的以太或任何类型的有价值的投资,并使得任何恶意活动的负面后果都超过攻击网络的好处。

例如,如果我想成为矿工以换取一些以太币作为我的时间和资源,我必须在运行 PoS 协议的智能合约中锁定 100 个以太币。 如果我决定验证无效的区块或交易,而某人通过多个安全机制注意到我的恶意行为,我将失去所有这 100 个以太币,并且无法再次发动攻击。 成功处理一个块的奖励将是投入的资源的一部分,例如 0.1 ETH。 这迫使节点合作并负责行事,以避免失去大笔赌注,即使他们同意攻击系统。

或者,委托权益证明DPoS)可以在以太坊的后续版本中使用。 它涉及通过投票将交易的验证委托给其他节点。 它在 BitShares 区块链中使用。

介绍以太坊虚拟机

以太坊虚拟机EVM)是一台虚拟机,允许执行带有有关燃气成本和价格的限制的代码,每个与之交互的个体都必须支付费用以保护网络免受垃圾邮件攻击,以便许多去中心化节点可以使用相同的软件进行交互。 它处理使用汇编代码生成的字节码,汇编代码又使用称为操作码的指令(opcodes)。 这是一台图灵完备的计算机。

当我说图灵完备时,我的意思是在以太坊上运行的智能合约编程语言具有以下特性:

  • 它们可以无限访问随机存取内存RAM

  • 它们可以根据内存中的可用信息做出决策

  • 它们可以在whilefor递归循环的帮助下永久运行

  • 它们可以使用函数

这意味着智能合约能够执行您给予它们的任何操作,只要有足够的时间和资源。 这一点很重要,以避免有人说以太坊是图灵完备的区块链时产生混淆。

介绍状态机

状态机是一种机制,用于跟踪区块链上发生的状态更改。 例如,一个正常的一天有两种简单状态,要么白天要么晚上。 状态机将记录每天的情况,以便当太阳下山时,白天的状态变为晚上。 这与一周中的日子一样。 每天可以是七种不同的状态之一,例如星期一或星期五。 每当在午夜时分发生变化时,跟踪一周中日期的状态将在状态机中更新。

状态机强制执行共识规则,以确保用户在拜占庭抗攻击系统中处理有效的交易:

  • P2P 网络:它连接参与者并传播已验证交易的事务和区块。这是区块链节点用来在它们之间传播信息以达成共识的网络。

  • 激励计划:在以太坊的情况下,该计划是 PoW,用于创建经济安全的状态机。以太坊开发人员计划转向 PoS 系统,其中用户将使用基于矿工在那一刻锁定的 ETH 数量的被动交易验证系统来处理交易。

  • 开源客户端:这个客户端是节点用来与区块链交互的客户端。在以太坊中,我们有 Geth、Parity 和许多其他客户端,允许您连接到区块链进行挖矿和处理交易以及下载区块链后执行各种任务。

您可以通过下载 Geth 客户端并运行 geth --fast 来下载区块链的轻量版,它只会跟踪每个区块的参考编号,以限制区块链的下载大小,因为它可能达到几百吉字节。轻客户端的目的是使以太坊区块链适用于具有有限存储和计算能力的低规格计算机。

P2P 网络

区块链运行在一个 P2P 网络之上,其中节点相互连接以交换数据和状态更新。由于这项技术,我们能够直接与其他计算机交互,以便处理订单,以便我们都同意区块生成系统。它允许矿工完成 PoW 挑战并获得奖励。

共识规则

当我们谈到共识时,我们指的是在拥有许多参与者的系统中使用的一组技术,以便让所有参与者就有利于整个基础系统的决策达成一致。虽然投票赋予了少数满足一系列要求的特定个体决策权,但共识考虑了每一个参与者,以达成全局思维的一致。

实现任何形式的区块链技术共识的每个算法必须提供以下功能:

  • 达成有利于整个系统的决策:这个想法是做出不仅有利于个人,而且有利于整个网络的选择,以便每个人都有一个更好的平台。

  • 允许开放参与:每个人都应该完全自由地加入并做出他们认为会是积极的决定。

  • 足够安全,以至于恶意行为者无法损害系统:所有共识协议都必须朝着系统的改进方向,其中恶意用户无法有足够的权力为其他人做出决定。

达成共识的问题始于所谓的拜占庭将军问题,这个问题在于许多计算机不容易达成预定顺序的一致性。一些计算机会迟收到指令,其他一些会因为不能从中受益而忽略它,而其他一些会尽量按照指令执行。

本质上,共识规则是必须实现全球认可的全球状态的,同时也是为了以有益的方式参与决策过程而获得奖励。

工作量证明

这是一种保证任务的结果很难完成的方法。为什么我们希望使处理任务变难?为了奖励那些最擅长完成它的人。在区块链中,处理来自使用网络的个人的交易需要处理能力,即硬件,能量和时间。它被用来以简单的过程生成区块:

  • 挖矿者提出一个包含最近区块头和 nonce 计数器的哈希数字的新区块。

  • 然后,挖矿者将哈希与由挖矿难度确定的目标值进行比较。

  • 如果哈希达到目标难度,用户将通过获得 ETH 来获得解决方案的奖励。如果没有,nonce 将被递增,直到生成具有期望解决方案的哈希。

由于挖矿者将与许多其他人竞争,所以必须有一个系统来确保区块时间的一致性,因为我们希望即使新节点加入挖矿网络,也能保持以相同周期生成区块。为了保证相似的区块时间,挖矿难度被创建了。

股权证明

PoS 是一种新的算法,用于在去中心化区块链中节点之间达成共识,重点是在保持网络免受攻击和恶意行为的同时,消除 PoW 对高计算性能的要求。它的工作方式是基于锁定 Ether 作为押注,并验证可能失去押注的区块,如果你行为不端。以下是该过程:

  • 想参与 PoS 过程的人被称为验证者。他们首先将一定数量的代币锁定为他们的押注(例如,100 个 Ether),在他们挖矿时不能触及。这应该足够昂贵,可以确保一群人不会同意以攻击系统的方式冒险,如果他们不成功会失去他们的押注。

  • 然后,他们开始接收使用以太坊区块链的交易。验证者运行程序来验证他们收到的交易是否有效,以便可以按顺序将交易分组创建新的区块。当他们有足够的交易来覆盖每个区块的 gas 限制时,他们将对这些区块下注 - 例如,从最初押注的 100 Ether 中押注 20 Ether。

  • 下注最多的区块被选为赢家。

  • 当从生成的区块池中选择获胜区块时,对该有效区块进行押注的矿工将根据他们的押注比例获得该区块使用的总 gas 的一部分。他们不是以区块奖励的形式获得报酬,而是以交易费的形式。例如,如果所选区块有 100 笔交易,累积了总共 0.5 以太币的交易费用,那么这 0.5 以太币将必须在所有押注该区块的验证者之间分配。如果该区块从 10 个用户处获得了总共 1,000 以太币的押注,而你押注了 20 以太币,那么你将获得 0.5 以太币的 2%,因为这是你为该区块押注的金额。总体上,你将获得 0.01 以太币。

在为区块进行押注时,你不会损失任何资金,它只是一个指示器,表明你对该区块的有效性有多少信任。考虑到没有区块奖励,只有费用,这可能看起来是一个小奖励,但你必须考虑到这些区块是在几秒钟内生成的。甚至可能每秒一个区块,这最终会在一天内产生大量的利润。

从理论上看,这听起来很好,但有一个主要障碍阻止了 PoS 完全可行。它被称为 无所谓的押注(Nothing at Stake) 问题,并在以下场景中展示,在该场景中,存在一个主链和一个使用 PoS 创建的新链:

当涉及到 PoW 时,你可以在任何你想要的分叉上进行挖矿而不会有任何风险,因为你可以对每个看起来不错的区块进行押注。什么阻止你决定把所有的蛋放在一个新链上,从而生成一个硬分叉呢?

如果我们使用 PoW,你就不能轻易转移到一个新链上,因为你必须花费大量的处理能力和时间生成新的区块,只要人们决定接受它作为一个新的区块链。你将在很多个月内进行挖矿,但很难创建一个能够被接受为一种新币的新链,因为它的使用减少了,价值更低了。

但是使用 PoS,你可以简单地对所有看起来吸引人的区块进行分散投资,并且没有后果,因为只要你验证的是好的交易,你不会损失你的股份。你将在几个不同的区块链上生成区块而没有风险。这种可能性可能会导致创建数百个不同的区块链,因为人们可以同时在所有这些区块链上进行挖矿。这就是为什么被称为 无所谓的押注(Nothing at Stake) 问题,因为当参与挖矿过程时,你不会失去任何东西。

这是以太坊近期无法完全转向 PoS 的主要原因之一。

以太坊的研究人员一直在努力解决这个问题。其中之一被称为Casper的提案旨在通过惩罚那些无意义地在新链上挖矿的用户,从而将他们的股份收回,以便用户集中于对最长链进行投丨注。Casper 还惩罚不积极参与网络的节点,以免它们在不提供价值的情况下开始消耗资源。

归根结底,PoW 必须改进或完全改变才能变得可持续,因为许多大型矿池开始获得过多的权力。我们现在处于这样一个阶段,四家矿业公司可以获得约 60%的总交易力量,使它们有权进行 51%攻击,并强迫所有用户按照它们自己的条款和自己的腐败区块链挖矿。 PoS 将会长期存在,并再次带来平衡。

智能合约的工作原理

智能合约是区块链应用,可以执行代码并执行各种不同的任务。这些是矿工在挖矿时执行的程序。它们是安全且不可阻挡的协议,会自动执行和强制执行。您可能已经对它们很熟悉,因为本书面向以太坊开发人员;然而,如果您不知道如何使用它们,或者想更多了解它们,可以放心,因为您将在适当的时候得到详细的解释,从而全面理解一切。

与传统的纸质合同不同,以太坊的智能合约在现实世界中没有法律后果,并且在它们违反时无法被法律实体强制执行。相反,它们依赖于代码即法律的原则,这意味着代码是规范合约行为的唯一依据。您不能执行未在智能合约中定义的功能,并且必须遵守每个功能的要求。

有趣的是,智能合约允许创建不可阻挡的应用程序,这些应用程序将在区块链的顶部保持数据和功能运行,而不管底层网络界面是否不可用或因某种原因被审查。智能合约为一种新类型的应用程序打开了世界,您可以部署并忘记,知道它将在任何情况下运行。

关于代码是否可以作为法律合同在法庭上受到认可存在着持续的争论。一方认为智能合约必须进一步发展,并在法律上加以执行,以获得更广泛的应用视角,而另一方则认为代码即法律足以保证协议的安全性和执行。

智能合约是在软件层的区块链顶部运行的程序。它们允许开发人员创建去中心化的无信任程序,具有管理个体之间交易的特定功能。它们不依赖于集中式服务器,而是在去中心化应用程序中充当数据库和服务器。

从本质上讲,智能合约在其容量上必须受到限制,因为我们正在执行的代码将永久地作为区块链上的一笔交易,这意味着您执行的每个函数都会在区块链上注册,而没有撤消已完成的操作的选项。这就施加了一些自然的限制,因为您不能只是修改每个以太坊节点都必须下载的公共数据库而不受任何限制。每次更改都必须由网络的所有节点下载。

它们的目的是解决许多公司在处理投票、银行业务以及人们在不公开他们对你的数据和金钱做了什么的情况下所面临的信任问题。它们还提供了可访问性,因为任何有互联网连接的人都能够访问它们并检索存储在内部的数据。

然而,智能合约在其所能做的事情上受到限制。对于许多情况来说,它们并不是一个很好的解决方案,比如以下情况:

  • 需要一个能够以快速速度处理信息的高效后端的应用程序,比如创建用户和与应用程序交互。它们天生就慢,因为它们依赖于以太坊的区块时间,并且它们无法在实时情况下工作。

  • 存储大量数据的应用程序在数据库上。智能合约的工作原理类似于数据库,因为它们允许任何人在区块链上存储特定信息,但它们受限且成本高昂。

Gas 是另一个很重要的概念,适当地理解它至关重要。它的创建是因为智能合约是图灵完备的,这意味着它们可以执行无限循环的whilefor,直到完成任务。开发者可能会创建永远停留在相同状态的无限循环。为了避免成千上万的节点被困在处理一笔交易而没有限制的情况,每笔交易都有一个取决于执行它所需的处理能力的 gas 成本。

因此,他们创建了一个系统,你在使用区块链进行每一项操作时都需要付费。你可以在区块链上存储信息,但你将不得不支付重要的成本,因为使用它的每一个单节点都将不得不下载你的更改。它计算 gas 成本的方式很简单:

  1. 你支付了你愿意为你的交易花费的最大 gas。

  2. 您的本地以太坊节点通过检查用于生成所需计算能力的操作码来计算执行您的交易将花费多少。您知道,使用区块链的每个小操作都可以被测量,因为我们使用一种告诉我们何时正在使用这台超级计算机以及如何使用的一种汇编语言。

  3. 使用了正确数量的 gas,并且剩余的部分会退还给您。

您还需要确定一个从 1 到通常 100 或更高范围的燃气价格,以告诉矿工首先处理哪些交易,因为每个燃气越昂贵,他们就可以得到更多报酬。

智能合约部署过程

智能合约类似于在以太坊区块链上运行的服务器端 Web 应用程序。然而,部署智能合约的过程却大不相同。要成为一名专业的以太坊开发者,你必须理解智能合约代码是如何在区块链块中处理和存储的,以便你能够使用它们,因为这将让你了解在推送你新编写的智能合约代码时可能出现问题的原因和方法。

在尝试部署代码到区块链时收到错误并不罕见,所以要成功调试它,你需要理解底层发生了什么。

以太坊交易

要了解智能合约如何部署到网络上,首先要了解交易的工作原理是很重要的,因为当你部署一个智能合约时,你实际上是生成了一个带有你刚刚构建的应用程序的字节码的交易。

在以太坊上组成交易的消息由以下编码组件组成:

  • 收件人:这是将接收交易的以太坊地址。

  • 价值:这表示要转移到收件人地址的以太量。这个值可以是零,并且你可以在 Solidity 中使用全局 msg.value 变量来访问它。该值始终以以太为单位,是以太坊中的最小单位。

  • 数据:这是一个十六进制的字节码字符串,主要用于调用具有所需参数的特定函数。这是你需要智能合约执行的具体信息。当智能合约彼此通信时,它们需要一种方式来告诉何时以给定的一组变量执行特定函数。由于这个数据参数,我们可以在交易处理时编码我们想要在合约中调用的函数。另一方面,当智能合约首次部署到区块链时,数据参数包含了智能合约转换为字节码的内容,以便机器能够理解它。总的来说,它包含了智能合约函数将由矿工在下一个区块中执行的信息。

  • 燃气限额:这代表了燃气限额,即你愿意为处理你的函数交易支付多少燃气。燃气限额以 wei 表示,给矿工尽可能多的燃气来处理你的代码是强制性的。

  • 燃气价格:燃气价格决定了你提供的每个燃气的成本。如果你的燃气成本为 1,你将支付每个燃气 1 wei。如果是 20,你将支付每 1 个燃气 20 wei。它用于帮助矿工处理交易,因为他们将部分通过交易费用获得奖励。

  • Nonce:Nonce 是一个用于标识交易的唯一计数器号码。这个唯一计数器用于标识每个区块,并帮助矿工识别无效区块,因为 Nonce 必须始终比上一个区块的数字大一个数。

  • 签名:这是由三个独立变量组成的参数,称为vrs。这些变量用于使用你的唯一以太坊地址数据签署交易,以便人们可以确认你是创建者。

当用户向智能合约发出交易时,称为消息而不是交易。交易和消息之间的区别存在,因为消息没有签名数据,因为它们不需要被对方签名。Nonce 是必需的,以防止重放攻击,其中外部用户可以获取相同的交易数据并为自己的利益再次执行它。

当你部署一个智能合约时,实际上你是向地址0x0发送一个交易,带有一个特殊的字节码标识符,以便矿工理解你正在创建一个新的智能合约。在这种情况下,数据参数包含所有智能合约逻辑,包括函数名称和参数。

总之,创建和使用智能合约是一个透明的过程,你告诉矿工处理你的数据。然后他们将理解其背后的字节码,并使用所需的参数对区块链进行必要的更改。

必要的智能合约编程

在了解了智能合约是如何创建的以及它们如何融入区块链系统后,你现在将更加实际地了解如何使用两种最流行的语言——SolidityVyper来创建智能合约。

Solidity

Solidity 是为在以太坊网络上开发智能合约而创建的最先进的编程语言。它的语法类似于 JavaScript,但具有静态类型的变量和函数。它提供了简单的功能,如函数、循环和几种类型的变量,以及复杂的功能,如汇编、加密函数和签名验证系统。

它已经在许多项目中被使用,特别是 ICO,在取得了巨大成功,因此足够成熟,可以被任何对开发去中心化应用程序和安全智能合约感兴趣的开发者使用。

其主要缺点是它更难以保护,因为它提供了更完整的功能集,这可能导致安全问题,如果不经过审核。

文件结构

我们将在本书的所有示例中使用 Solidity 的 0.5.0 版本。Solidity 中的智能合约始终从文件中使用的版本开始,以确保合约与可能因新增功能而破坏合约的新版本不兼容。

让我们通过以下步骤来看一下 Solidity 合约的结构:

  1. 你在文件开始使用pragma语句来定义版本:
pragma solidity 0.5.0;
  1. 然后你可以开始编写你的合同。Solidity 中的所有语句必须以分号(;)结尾才能有效。在文件中定义了使用的版本后,你必须创建合同,就像这样:
pragma solidity 0.5.0;
contract Example {}
  1. 你可以在一个文件中定义多个合同:
pragma solidity 0.5.0;
contract Example {}
contract Another {}
contract Token {}
contract ICO {}
  1. 在合同内,你将拥有状态变量,函数,修改器和一个构造函数。稍后我会详细解释它们的用法:
pragma solidity 0.5.0;
contract Example {
    uint256 counter;
    modifier onlyOwner {}
    constructor() {}
    function doSomething() {}
}
  1. 直接在合同中定义的变量,即在函数之外定义的变量,被称为状态变量。这些都是特殊的变量,即使在执行合同之后也会保留它们的值。把它们想象成特殊的永久性变量,你可以随时读取和修改它们:
pragma solidity 0.5.0;
contract Example {
    uint256 myStateVariable;
    string myOtherStateVariable;
    function example(){
        uint256 thisIsNotAStateVariable;
    }
}

正如你所看到的,它们位于函数之外但是在合同之内,并且它们是在文件顶部定义的,就在合同开始的地方。正如我所说,它们永远保持它们的值,即使在修改合同之后也是如此。所以,如果你的myStateVariable的值为5,你可以在修改它之后的几天或几个月后读取该变量的值,只要你不修改它。

它们直接存储在区块链存储中,而不是在内存中。如你稍后所学,内存变量会在合同执行后失去值并被重置。

最后,Solidity 文件使用.sol扩展名,例如,example.sol。你将学习如何使用Remix IDETruffle部署它们,在第三章掌握智能合同第九章去中心化交易工作流中。

变量

Solidity 是一种静态类型的语言,这意味着你必须为你创建的每个变量定义类型。

让我们先定义这种编程语言中可用的变量类型,以便后来理解如何使用它们,但在此之前,你需要了解变量的可见性。

变量的可见性

Solidity 中的每个变量和函数都有特定的可见性。可见性是你在变量类型后使用的关键字,用来定义谁可以访问它:

  • 公共:这意味着该变量可以被任何合同读取或写入,包括外部合同,只要有一个函数来更新它们。

  • 私有:私有变量无法被派生的智能合同访问,那些用is关键字实现你的合同的智能合同;例如,contract Example is Another {},其中Another是一个带有私有变量的智能合同,无法被Example访问。

  • 外部:这些变量和函数不能被包含它们的合同访问。只有外部合同和用户可以使用它们。

  • 内部:这些是无法被外部实体读取或写入的变量和函数,只能被合同本身或继承的合同访问,就像你在私有变量的例子中看到的那样。

为了保持简单,我建议你对变量始终写上public,除非它是一个特殊的变量,这种情况并不经常发生。如果你不定义任何可见性,变量默认是public,虽然最好每个变量都写上public关键字,以确保你理解变量的可见性,并且这不是一个错误。

Uints

Uints 是无符号整数,意味着它们是从零开始的数字,不能为负数。

你定义它们如下:

uint public myNumber;

如你所见,你首先定义变量的类型,然后是可见性,然后是变量的名称。请记住,如果你不定义变量的可见性,变量将是public

Uints 可以是以下类型之一:

  • uint8

  • uint16

  • uint24

  • uint32

  • uint64

  • uint128

  • uint256

每个变量的数字表示uint的大小。一个uint8类型的变量能够存储最多 256。因此,uint8变量的最大值是256。如果你想存储数字 255,变量将正常工作,但是,如果你想存储数字256如下,那么变量将溢出,并且会重置为零而不是256,因为它超出了该类型变量的容量:

uint8 public myNumber = 256;

当你尝试存储一个值256时,变量会重置,因为它从零开始,所以容量是计算出来的数字减去一。

当你尝试存储另一个超过变量容量的数字时,比如300,变量的值将会是44。所以,输入以下内容:

uint8 public myNumber = 300;

它将变成以下内容:

uint8 public myNumber = 44;

请注意,你不能给变量赋一个超过其容量的值,因为在某些情况下,尝试部署你的合约时会得到编译错误。当函数接收到一个uint8,但用户输入的值大于 255 时,就会出现溢出问题。

这与uint16是一样的,它的最大值是65536-1。同样,uint24的最大值是16777216-1uint32变量的最大值是4294967296-1uint64变量的最大值是1844674407370955e19-1uint128变量的最大值是3402823669209385e38-1uint256变量的最大值是1157920892373163e77-1

正如你所看到的,最大数字增长得相当快。这对于处理大数字时避免溢出非常有用。

另一方面,你还会遇到欠流的问题。当你尝试将一个负数存储到uint中时,就会发生这种情况。例如,尝试执行以下操作:

uint8 public myNumber = -5;

你会得到以下结果:

uint8 public myNumber = 251;

这是因为溢出的原因发生的。你从零开始,一直加到变量能够容纳的最大值。

这些问题可能导致严重的漏洞。这就是为什么重要的是检查用户在函数中输入的值是否在可接受数字范围内。稍后你将看到如何使用一个名为require()的全局函数来验证函数的输入。

地址

在 Solidity 0.4.0中,只有一种地址类型。现在,我们有两种类型来定义以太坊地址是否应该是payable的。

一个地址包含每个以太坊用户的账号编号。它是一个由42个十六进制文本字符组成的文本,例如这样一个:

0xeF5781A2c04113e29bE5724ae6E30bC287610007

要在您的合同中创建一个地址变量,您必须定义如下:

pragma solidity 0.5.0;
contract Example {
    address public myAddress = 0xeF5781A2c04113e29bE5724ae6E30bC287610007;
}

地址没有引号,因为它们不是文本字符串。在这个版本的 Solidity 中,你必须定义地址的类型,它可以是以下之一:

  • 地址 payable 可支付地址是在 Solidity 0.5中引入的一种新的变量类型,允许该地址接收和存储以太币。以前,所有地址都是payable,现在只有那些明确标记为payable的地址才能接收或发送以太币,并且使用处理以太币的函数,如.transfer().send()

  • 地址: 一个普通地址,无法接收或发送以太币,以防止用户这样做。

您可以将payable地址定义如下:

address payable public myAddress;

当您想要向该地址发送以太币时,拥有payable地址将非常有用。例如,假设用户 A 想从智能合约存储的余额中收到10个以太币。他们将执行以下操作:

pragma solidity 0.5.0;
contract TransferExample {
    address payable public userAAddress;
    function transferFunds() public {
        userAAddress.transfer(10 ether);
    }
}

因此,用户 A 将从存储在此智能合约中的资金中收到10个以太币。

地址的另一个重要方面是,有时您需要访问当前智能合约的地址,因为,正如您所知,智能合约可以在内部持有以太币。

要获取您的智能合约地址,请使用以下代码:

address public myContractAddress = address(this);

在这里,this是一个特殊关键词,用于引用当前正在使用的活动智能合约。但因为它是智能合约的一个实例,您需要使用类型转换函数将该实例转换为地址,该函数本质上是获取本智能合约的地址。

您还可以使用.balance函数访问此智能合约的余额,如下所示:

uint256 public myContractBalance = address(this).balance;

这将返回智能合约中的 wei 数量,可用于使用transfer()函数进行转账:

myUserAddress.transfer(address(this).balance);

这将向myUserAddress发送存储在此合约中的所有以太币。

您可以将payable地址转换为普通地址,但反之则不行,基于payable地址是具有额外函数的增强版本,无法轻松传递。

字符串和字节

字符串和字节以单引号或双引号形式存储文本片段,如下所示:

string public myText = “This is a long text”;
bytes public myTextTwo = “This is another text”;

它们允许您存储大约1,000个字,并且它们本质上是相同的。您可以有更小的字节变体,比如bytes1bytes2bytes3,一直到bytes32

现在,bytes32是一种有趣的变量类型,因为它允许你以非常紧凑和高效的方式存储约 32 个字符的文本。它们在需要短文本的许多情况下使用:

bytes32 public shortText = “Short text.”;

它们在许多其他高级用例中使用,例如检查字符串或字节的文本是否为空。例如,如果你有一个接收文本的函数,你可能希望确保文本不为空。你可以这样做:

function example(string memory myText) public {
 require(bytes(myText)[0] != 0);
}

不要担心函数的技术细节。如果你还不知道或记不住它们,要检查字符串是否为空,你必须执行以下操作:

require(bytes(yourString)[0] != 0);

这告诉合约确保字符串的第一个字母不为空。这是检查空字符串的正确方法。我们对字节执行相同的操作,但不进行字节转换。

在以太坊字符串中添加特殊字符时使用它们。

结构体

如果你熟悉 JavaScript,你可以将结构想象成具有属性和值的对象。struct看起来类似于以下内容:

struct Example {
    propertyOne;
};

枚举

枚举是具有唯一名称的固定大小列表,由你定义。你可以将它们用作特定对象的自定义修饰符,或者在智能合约中保持特定状态。这对于控制 ICO 的状态非常理想。

你声明它们如下:

enum Trees { RedTree, BlueTree, GreenTree, YellowTree }

然后,你创建enum变量:

Trees public myFavoriteTree = Trees.RedTree;

注意,在enum声明的末尾不必添加分号,但是对于刚创建的具有enum类型的变量,你必须添加分号。

布尔值

布尔变量可以是truefalse

bool public isValid = true;

数组

数组允许你将大量相同类型的变量存储在一个地方。它们被用作包含特定类型信息的列表,以便在智能合约中有序存储数据。你可以通过简单的for循环获取它们的长度来访问它们。

你可以创建 uints、strings、structs、addresses 等几乎任何其他类型的数组:

uint256[] public myNumbers;
string[] public myTexts;

你也可以使用以下关键字从数组中删除元素:

delete myTexts[2];

你也可以使用.push().pop()向动态大小数组中添加或删除元素。

映射

映射是一种特殊类型的变量,它们可以容纳无限量的数据。它就像数组和结构的组合。你可以为一组类型添加元素:

mapping(string => bool) public validStrings;

映射将信息存储为无限数组。它们的工作方式类似于 JavaScript 中的对象,其中每个键都有一个关联的值,并且它们可以被随机访问。它们没有固定长度,也不能像数组那样获取它们的长度来循环它们的值。你必须做的是保存映射的最新更新键,然后从那里开始。

你可以如下设置映射的值:

validStrings['example'] = true;

在我们的例子中,validStrings的所有值都将是false,直到你将它们设置为true

数据位置

在 Solidity 中,您可以选择定义变量存储的位置。您可以决定将它们存储在以下位置:

  • 存储:这是在区块链上写入的永久位置,因此使用起来很昂贵

  • 内存:这是一个非永久的地方,变量只在智能合约运行的时间内保存。

  • 调用数据:这是msg对象数据信息存储的地方,一个专门的全局变量

这种例子在以下代码中可以看到:

uint256 memory myNumber;
string storage myText;

事件

事件是一种特殊类型的函数。它们的目的是在区块链上记录数据和您希望在以后检索的操作。它们可以订阅以在产生新事件时接收更新,几乎是实时的。

本质上,您希望它们保留智能合约内部发生的事情的注册表,以便以后分析它们以修复错误,并了解如果需要以轻松的方式阅读过去发生了什么。

以下是如何在 Solidity 智能合约中声明事件的方式:

pragma solidity 0.5.0
contract EventsExample {
    event LogUserAddress(address userAddress);
    function registerUser() public {
        emit LogUserAddress(msg.sender);
    }
}

在这个例子中,您可以看到如何声明和发出一个事件。当您声明事件时,您必须决定它能够接收的参数;它们始终是可选的,因此您可以省略它们。

当您在函数内部发出事件时,必须确保它们是正确的类型。在声明中,您可以为每个参数添加名称,或者只留下类型,如下所示:

event LogUserAddress(address);

好的做法是在事件内部命名参数,以帮助他人理解每个参数的目的。

您还可以添加一个名为indexed的可选关键字。它是事件参数的修饰符,允许您搜索过去的事件以获得特定事件。将indexed参数视为数据库中可搜索的条目:

event LogUserAddress(address indexed userAddress);

请注意,您必须为indexed的参数命名。稍后您将看到如何使用 web3.js 检索这些事件并搜索特定事件。

修饰符

修饰符是一种特殊类型的函数,用于在当前函数执行之前验证数据或执行某些操作作为中间件。它们主要用于验证执行函数的用户是否具有所需权限,并验证参数:

address public owner;
modifier onlyOwner() {
    require(msg.sender == owner, ‘You must be the owner');
    _;
}

function doSomething() public onlyOwner {}

正如您所见,onlyOwner修饰符用于检查doSomething函数是由合同所有者执行还是由其他用户执行。如果调用者是所有者,则函数被执行,如果是外部地址,则合同将恢复,抛出异常。

请注意,require()函数是一个全局断言函数,用于验证其中的条件是否为真。如果条件不为真,它将抛出异常,停止执行智能合约。

_修饰符内部的下划线语句用于指示函数的代码将在哪里执行。有时,你希望在修饰符的检查之前执行函数。在修饰符中,下划线语句是强制性的。此外,请注意,修饰符可以选择性地具有参数。如果不需要参数,你可以将括号删除如下所示:

modifier onlyOwner { ... }

修饰符是非常强大的工具,当你看到重复的代码为多个函数执行相同的验证时,你经常会使用它们。

在下一节中,你将看到函数可以采用的用于可见性和支付的特殊修饰符的类型。

函数

Solidity 中的函数在语法上与 JavaScript 中的函数非常相似,但它们具有一些您必须了解的关键区别,例如您必须指定返回类型、函数的可见性以及适用于每个特定函数的修饰符(如果有的话)。语法如下:

function example() public returns(uint256) { }

函数与变量一样具有可见性,其中公共函数可以被外部用户、合约以及合约本身执行。外部函数只能由外部实体执行,而不是由合约本身执行。内部函数只能由包含合约执行。私有函数只能在当前合约内部执行,或由继承合约执行。

现在,一个函数可以具有确定其类型的特殊修饰符。这包括以下修饰符:

  • 视图:视图函数是一种不修改状态变量但可以读取它们的函数。请记住,状态变量在合约开始处声明,并且它们用于直接在区块链上存储信息。因此,如果你的函数不修改任何状态变量,你必须将其标记为view

  • 纯函数:纯函数更为严格。它适用于那些甚至不读取状态变量的函数。纯函数通常是在函数内部进行某种类型的计算而无需依赖外部数据的函数。这通常包括数学函数或格式化函数。

  • 可支付函数可支付(payable)函数在执行时能够接收以太币。它会将以太币存储在合约内部,因此非常重要的是你要创建系统来提取发送到智能合约的以太币,否则这笔钱将永远被困在里面。如果你的函数没有标记为可支付(payable),当你在函数执行过程中发送以太币时,你会收到一个错误并且交易会回滚。

如下是其外观:

string public myStateString = 'Hi';
function exampleOfView() public view returns(string memory) {
    return myStateString;
}

在那个函数中,我们只是读取并返回myStateString状态变量,因此我们可以将其标记为view。请注意,对于字符串类型,我们必须使用memory关键字,因为它们在内部是一种数组类型,就像一个个单独的字符数组一样。

另一个示例如下:

function sumTwoNumbers(uint256 numberA, uint256 numberB) public pure returns(uint256) {
    uint256 result = numberA + numberB;
    return result;
}

这个pure函数只是将两个数字相加并将结果返回给调用者。它不修改状态,也不读取状态变量。

这是一个payable函数:

function receiveDonation() public payable {}

receiveDonation函数是空的,因为我们只需要接收以太币。我们不必对其做任何处理。

回退函数

这是一种没有名称的特殊类型的函数。每当有人使用.send().transfer()函数发送以太币到此合约地址时,它都会被执行,类似于默认函数。它经常在 ICO 中用于接收以太币并返回收到的以太币的指定数量的代币。这允许任何人购买代币,而无需理解和部署合约实例,只需合约的地址。

它看起来是这样的:

function () external payable {}

回退函数必须标记为external,以帮助人们理解不应该错误地在此合约内执行它。如果你不添加payable修饰符,它将拒绝所有发送以太币的交易。

我建议你应该使用remix.ethereum.org IDE 编写并尝试不同的函数,它将向你显示必须验证的错误和通知。这样,你就能安全地编写自己的合约。

Vyper

Vyper 是一种新的智能合约编程语言,其语法类似于 Python。它是由维塔利克亲自创建的,对于新的去中心化应用来说,它是最有趣的选择之一,因为它提供了一种与传统 Solidity 智能合约不同的方法。

它的目标是成为一种简单的编程语言,具有基于简单性的增强安全性,代码应该易于理解,即使对于非开发人员也是如此。这就是为什么语法如此简约的原因。他们还希望它成为一种编程语言,越来越难编写出有缺陷或易受攻击的代码,以便开发人员不必花费无数小时分析每个应用程序的安全性,同时默认避免意外的漏洞。

这就是 Vyper 添加了几个有趣功能的主要原因,例如以下内容:

  • 每次函数调用将花费多少 gas 的了解:精确指示气体成本是重要的,因为您希望用户能够准确计算每笔交易将投入多少以太币。它既节省了人们的钱,又使程序可预测。

  • 自动漏洞检查:溢出、下溢、重入攻击和许多其他众所周知的漏洞在 Vyper 中自动修复,无需手动注意每个智能合约函数。

另一方面,他们移除了其他智能合约编程语言(如 Solidity)中的重要特征:

  • 不再使用修饰符: 在 Vyper 中不允许使用修饰符,因为它们使代码难以阅读,你必须来回跳转到修饰符定义和使用之间。此外,它们可能被恶意使用,通过意外执行代码;例如,创建一个名为 onlyOwner 的修饰符,但随后执行与其名称预期完全不相关的 transfer() 函数。

  • 不再使用汇编: 汇编代码很难理解,即使对经验丰富的开发人员来说也是如此,因为你正在处理非常低级的函数,可能会产生误导。这意味着你将无法创建使用签名、状态通道等依赖于汇编的智能合约。

  • 不再使用递归函数: 为了避免重入攻击并确保精确计算气体成本,他们移除了函数可以调用自身不确定次数的递归功能。

一般来说,Vyper 是一种功能强大的语言,非常适合较小的项目,并且不需要高级功能,例如汇编。你可以快速创建易于维护的智能合约,用户将在几分钟内就能理解其轻量级语法和简约的代码。

以示例来说明 Vyper

为了快速掌握这种新的令人兴奋的语言,我们将通过一个简单的智能合约来进行,这样你就可以看到 Vyper 的完整复杂性。我们将在一个单一的智能合约中看到所有类型的变量和函数。

接着创建一个新的 example.vy 文件。正如你所见,Vyper 智能合约的后缀是 .vy。在其中,输入以下代码;我们稍后会解释每个变量的含义以及它们如何使用。这只是一个快速的练习,让你的编程手熟悉 Vyper 的编写方式。这段代码将成为你熟悉 Vyper 语法的指南:

# Events
LogTransfer: event({from: indexed(address), to: indexed(address), amount: uint256})

# Custom units
units: {
    kg: "kilogram"
}

# Numbers
myPositiveInteger: uint256
myDecimalNumber: decimal
myInteger: int256

# Addresses
owner: address

# Strings
myName: bytes32
myLongArticle: bytes[1000]

# Booleans
isThisTrue: bool

# Mappings
todoList: map(uint256, bytes32)

# Structs
struct Client:
    name: bytes32
    age: uint256

myClients: Client[100]

# Timestamps
myBirthday: timestamp
expirationTime: timedelta

# Wei value
etherToSpend: wei_value

# Custom unit types
myKilo: uint256(kg)

# Public functions
@public
def subNumbers(first: int128, second: int128) -> int128:
    return first - second

# Payable functions
@public
@payable
def transferFunds(_from: address, to: address, amount: uint256):
    log.LogTransfer(_from, to, amount)

# Functions that update state
@public
def updateBoolean(result: bool):
    self.isThisTrue = result

# Constructor
@public
def __init__():
    self.owner = msg.sender

# Fallback function
@public
@payable
def __default__():
    self.myBirthday = now

这些是一些澄清,帮助你理解正在发生的事情:

  • 事件必须在 Vyper 文件的顶部声明,并且它们必须放在大括号内,就像这样:({})

  • 无符号整数只能是正数,它们的最大值是 2**256。你不能有 uint8 或等效的;所有的 uint 必须是 uint256

  • 有符号整数、普通整数,可以是正数或负数,最大值是从两边各 2**128。它们只能是 int128,所以你不能有更小的大小。

  • 小数点的精度为 10 位小数,这意味着你可以在小数点后有最多 10 个字符,例如,1.2394837662

  • 字符串可以是 bytes32 或带有自定义大小的 byte 数组,例如 bytes[2000]。注意,你没有 string 类型的变量,因此上传后你的字符串将被存储为十六进制文本。此外,你不能有可变大小的字节,例如 bytes[],因为它们在计算气体成本时会产生不确定性。

  • 映射必须使用 map() 函数声明,并且它们可以用方括号访问,例如,todoList[3] = "Start something"

  • Timestamp是用于事件的固定日期,例如您的生日,或者将来的特定时间。这主要用作日期容器。timedelta更像是一个没有在日历中具体日期的计数器。例如,timedelta可以存储2 months,而时间戳可以存储2019 年 1 月 1 日,都以数字格式表示。

  • wei 值是用于以太币在 wei 中存储的类型。

  • 自定义单位类型是您在文件顶部定义的个性化类型,然后可以用于您的变量。它们必须转换为 uints、ints 或 decimals。

  • 函数可以是公共的或私有的。公共函数必须在顶部加上@public装饰器。函数的返回值用箭头符号->指定。

  • 支付函数必须使用@payable装饰器,并且可以访问与msg.value一起发送的以太币(如果有)。

  • 要在函数内更新状态变量,您必须在它们前面使用self.关键字将它们更新为状态变量。在 Vyper 中,您不需要在参数名称前面添加下划线_,因为您可以轻松地引用与状态中相同名称的变量,而在 Solidity 中您不能这样做,因此您必须使用下划线来区分它们。

  • 构造函数称为__init__(),而回退函数称为__default__()

就是这样!您可以在几页中掌握 Vyper 的最快方式。确保您将这个指南放在一个特殊的地方,以便在编写 Vyper 合约时节省数小时的头痛和不舒服的时刻。

概要

在本章中,我们首先对以太坊的历史进行了高层次的概述,以了解它的起源和发展方向。然后,我们转向特定主题,如 EEA,因为了解以太坊技术如何应用于不同的实际场景非常重要。之后,我们涵盖了许多关于以太坊区块链的具体主题,以更深入地理解作为开发人员有意义的技术层面,因为我们将处理与其区块链相关的所有方面。接下来,我们转向关于智能合约的更多技术主题,因为它们是以太坊开发人员所做的核心内容,以便您清楚地了解它们在整个计划中是如何实现的。最后,我们转向使用 Solidity 和 Vyper 进行基本智能合约编程,以启动您对这些最受欢迎的语言在现实世界中的使用方式的理解,为开始您的成为一名优秀以太坊开发人员的旅程奠定坚实的基础。

如果您不熟悉 Vyper,现在应该能够使用在线编译器编写简单的合约了,该编译器位于:vyper.online/。确保您自己练习本章中描述的函数,并在您对所解释的概念有直观的理解后继续阅读。

在接下来的章节中,我们将深入探讨以太坊架构,以深刻理解每个组件在基本层面上的工作方式,以及区块链技术背后的主要推动力。

第二章:以太坊生态系统

在本章中,我们将涵盖区块链的几个重要方面。例如,除了其他有趣的话题之外,我们将研究使有组织的、去中心化的计算机网络成为可能的技术发明,并探索其背后的经济学,以帮助您解决重要问题,为什么这项技术值得真正的金钱?这将帮助您成为这一领域去中心化应用程序的专家。您将能够比以往更好地解释所有这些如何共同运作,以便您能够引导您的未来客户走向正确的方向,因为您将了解到底层协议、技术和密码学层之后的以太坊区块链上发生了什么,并且您将下意识地解决您对这一发明的燃烧问题。

在本章中,我们将讨论以下主题:

  • 引入以太坊链规范

  • 区块链技术

  • 区块链共识

  • 区块链经济学

  • 区块链指标

引入以太坊链规范

以太坊链规范是一种描述以太坊区块链应该如何看起来的格式。它描述了实际上被视为以太坊区块链的参数、组件和元素应该具备什么,以便您可以创建具有不同属性的私有以太坊区块链,用于测试您的应用程序或分叉新版本的以太坊。规范定义了您需要创建自己的区块链所需的内容。例如,WhaleCoin 使用以太坊规范创建了一个新的区块链,该区块链具有相同的核心技术,但具有自己的修改,这要归功于以太坊链规范。

拥有一个清晰的规范非常重要,它告诉我们如何创建类似以太坊的区块链,因为硬分叉是每个区块链的基本部分,而且高级开发人员可能会感觉他们想要更多的控制权来测试他们的应用程序在私有区块链上。以太坊给了人们选择权,可以基于以太坊创建自己的系统;在我看来,这对于处理交易的开发人员或矿工来说真的很好,因为例如,您可以更改共识算法、每个交易的燃气成本、区块时间等,同时仍然具有有效的以太坊区块链。

使用以太坊链规范,您可以创建分叉或自己的私有以太坊区块链作为测试工具,以查看您的智能合约代码如何与现实世界中的交互。

创世对象

链规范规定以太坊区块链必须由一个称为创世块的单一块生成。这是一个特殊的块,因为它没有来自先前块的信息,而且它包含整个区块链的配置。

它基于一个 genesis.json 文件,并具有以下参数:

  • name: 这是链的名称。例如,HomesteadConstantinopleMorden

  • forkName: 这是这条链的可选次要名称。

  • engine: 这是一个枚举,指定共识引擎,可以是 EthashNull

  • params: 如果你只指定了 Ethash,这是一个包含共识引擎的几个属性的对象。不同的参数如下:

    • minimumDifficulty: 这是一个指定区块可能具有的最小难度的数字。

    • gasLimitBoundDivisor: 这是一个分隔符字符串。通常是 0x400,转换为 utf8 字符串后是字符 @。

    • difficultyBoundDivisor: 这是一个指定每个区块难度的整数,必须被二整除。例如,2084。

    • durationLimit: 这是增加难度的点。

    • blockReward: 这是发现以太坊区块的奖励。

    • registrar: 这是此链上注册人合约的以太坊地址。

不同的共识引擎可能允许 params 对象中的不同键,但是其中有一些是所有共同的:

  • accountStartNonce: 这个整数指定了所有新创建账户应该有的 nonce。

  • frontierCompatibilityModeLimit: 这个整数指定了 frontier 兼容模式完成的区块编号,以及 homestead 模式开始的区块编号。

  • maximumExtraDataSize: 这个整数指定头部的 extra_data 字段的最大大小(以字节为单位)。

  • minGasLimit: 这个整数指定了一个区块可能被限制的最小 gas 量。

  • networkID: 这个整数指定了这条链在网络上的索引。

  • genesis: 一个包含创世区块头的对象,就像你在 第一章 中看到的那样,区块链架构。头部包含了有关区块内容的具体信息,比如 gas 使用量、时间戳和 nonce。

genesis 对象包含了创建的第一个区块的一系列强制参数,即以下参数:

  • seal

  • difficulty: 难度

  • author

  • timestamp: 时间戳

  • parentHash: 父哈希

  • extraData: 额外数据

  • gasLimit: gas 限制

  • nodes: 一个包含区块链初始节点的字符串数组,以 enode 格式呈现。我们稍后会看到它的结构。

  • accounts: 一个带有创世区块账户的对象。每个账户必须有关于每个地址的几个键:

    • balance: 这个账户指定的以 wei 为单位的以太数量。

    • nonce: 在 genesis 时账户的 nonce,通常为零。

    • code: 与此账户或任何其他账户相关的合约的地址。

    • storage: 在 genesis 时帐户存储的十六进制编码整数的对象映射。

    • builtin: 用于指定账户代码是本地实现的替代代码。值是一个带有进一步字段的对象:

      • name: 执行的内置代码的名称,如 identityecrecover

      • pricing: 一个枚举,用于指定调用该合约的成本。

      • linear: 这指定了调用此合同的线性成本。值是一个对象,有两个字段:基本成本 base,以 Wei 为单位,并且总是支付;和每字成本 word,输入的每字的成本,向上取整。

现在您已经知道哪些参数放入 genesis.json 文件中,您可以开始创建自己的私有区块链,并为自己的个人应用程序自定义参数。在第三章,以太坊资产,您将学习创建自定义以太坊区块链的确切过程。

现在,您已经掌握了有关以太坊区块链如何启动、运行以及如何创建用于测试目的的个性化区块链的基本理解。

区块链技术

使区块链成为现实的技术是一组复杂的发明,它们共同努力实现一个全球协议的共同目标。要理解它们是如何相互关联的,最好将技术分解为四个主要部分,这样我们就可以看到所有分散应用运行的层:

使以太坊成为可能的是一组工具和协议,它们在社区中被广泛使用。您将能够在个人项目中使用这些工具,无论何时您想要实现非常难以从头开始重新创建的高级机制。

应用层

应用层是外部程序与区块链交互的层。它还包括智能合约语言,如 Solidity 和 Vyper。这一层是最重要的,因为它给了人们使用以太坊区块链做更多事情的选择,而不仅仅是发送和存储加密货币,它是关于分散技术用例的下一个步骤。以下图显示了应用层:

首先,我们有 钱包,它们允许普通用户管理私钥以便能够与 分散应用 (dApps) 进行交互并进行转账。

主要的是 MistMetaMaskMyEtherWallet

  • Mist 是一个正在开发中的浏览器,始终连接到以太坊。它允许您直接从中使用 dApps,而无需担心管理您的账户或使用外部插件。

  • MetaMask 是大多数浏览器上可用的插件,让您拥有连接到以太坊区块链的超能力,而无需拥有区块链的本地副本,因为它连接到一个名为 INFURA 的计算机网络,您可以在其中自由创建交易。

  • MyEtherWallet 是一个著名的以太坊钱包管理器,您可以在其中存储资金,连接到您的账户,并在线与智能合约进行交互。您也可以下载离线版本以增加安全性和可靠性。

接下来,我们有智能合约语言,如SolidityVyper。它们被用于创建与区块链交互的智能合约,其中包含字节码指令。这得益于矿工在挖矿时执行代码。Solidity 在 ICO、去中心化应用和代币中最常用,用户可以在无需信任的环境中与区块链进行交互。

接下来,我们有,如TruffleEmbark和**web3.js****。它们为您提供了在与智能合约交互的同时创建更好的 dApps 的工具:

  • Truffle:这个库以在最小可行产品MVPs)、dApps、ICO 和代币合约中被广泛应用而闻名,因为它为希望进一步发展的开发人员提供了安全而经过测试的环境。

  • Embark:这是由 Status 团队开发的一种 dApp 框架,为您提供诸如自动从.js代码部署合约、监视变化以及管理各种链等实用工具。

  • Web3.js:这是与现有智能合约在您的 Web 应用程序上通过 JavaScript 进行交互的最常用的库。它为您提供了直观和易于学习的简单接口。

最后,您有测试工具,用于验证智能合约的安全性。请记住,这些工具并不完美,因为它们只提供了扩展选项来测试您的代码,而无需强制您对去中心化应用的所有方面进行全面测试:

  • Sol-profiler:这是一个简单的分析工具,可生成一个美观的表格,以易于理解的方式显示所有您的合约和函数。对于较大的项目来说,当开始难以理解所有组件之间的关系时,这对您能够更有效地进行测试非常有用。

  • Solidity 覆盖率:这是一个用 JavaScript 编写的出色的测试工具,用于确保您的测试实际上检查了代码的每一行,至少保证进行了全面的测试,而不考虑质量测试。Truffle 是一个出色的测试工具,因为它为您提供了一个帮助您执行单元测试的测试框架。

  • Cliquebait:这为您提供了一个干净的 Docker 镜像,使用具有极快出块时间的权威证明以太坊区块链,以便您可以在没有依赖性的情况下部署和检查您的分布式应用程序的每个功能。

  • Hevm:使用 hevm,您可以获得以太坊虚拟机EVM)的定制实现,准确显示智能合约的运行情况,包括操作码和调试器,可直接与部署的智能合约进行交互。

基础设施层

在应用层以下,还有基础设施层,它提供一组更低级别的工具,以更紧密地与区块链交互。诸如去中心化存储、点对点P2P)实时消息传递和以太坊客户端等功能。

以下的示意图显示了基础设施层包含的内容:

因为区块链是建立在点对点(P2P)网络之上的,节点直接相互交互,我们可以利用消息工具,比如WhisperPyDevp2p

  • Whisper:这是由核心以太坊开发人员维护的一种协议,你可以在自己的 dApps 上使用它来发送几乎即时的消息。对于加密的聊天应用程序非常有用,因为你只交换哈希。

  • PyDevp2p:这是一个类似的工具,直接使用 DEVP2P 协议来交换消息,无需等待区块确认交易。它完全由 Python 编写,所以非常适合直接连接到以太坊的个人项目。

在这一层中,你还可以访问分散式存储,它可以让你在区块链上无限制地保存你的数据。这个协议非常类似于 BitTorrent,其中大文件直接在对等之间交换。

与以太坊合作的最流行的存储有IPFSSwarm

  • Swarm:这是建立在以太坊之上的、由核心开发人员维护的分散式存储。借助它,你可以创建能够上传特定类型数据并得到你内容的哈希的分散式应用。它的工作原理很简单;用户将一些文件上传到 Swarm 上,同时保持它们在线供其他用户使用,每当其他人想从中得到某些文件时,他只需直接从上传者那里下载,而无需请求许可。它提供了一个类似于互联网工作的容错系统,但没有了中心化。这个事实迫使用户自己保持内容的在线状态,当这项技术开始被普通互联网用户大规模使用时,这可能成为一个问题。

  • 星际文件系统IPFS):相比之下,这几乎与 Swarm 一样,在分散式协议存储大量数据,包括文件,在用户之间有一些明显的区别。首先,IPFS 专注于从网络中删除冗余。要知道,在分散式存储中的一个问题是,你将在不同节点上有大量相同的副本。

当你在不同处理时间的不同计算机上有相同的文件时,你只能每次选择最快的那个。你也无法确定你从网络上下载的文件是否真的是它所说的那样。它可能包含满是恶意软件的恶意软件。为了解决这个问题,IPFS 提供了类似于 GitHub 的系统,每个特定文件都有一个唯一的标识符。如果两个文件具有完全相同的内容,也就是它们是相同的,它们将具有完全相同的哈希值。

这非常好,因为这给人们提供了一个保证,他们可以得到未经任何修改的有效内容。这也增加了网络的可用性,因此您可以可靠地从许多不同的提供者那里获取正确的文件。IPFS 仍在不断发展,正在开发一种用于奖励用户将文件保留在网络上的硬币。

最后,我们有在以太坊之上运行的客户端。这是直接用于挖矿和下载公共以太坊链的软件。以太坊核心开发人员确保他们提供了各种类似的客户端,以便人们可以公开选择最符合他们利益的客户端,以避免出现一家软件垄断被强制推给所有人,带来的中心化问题。

这些是主要的客户端,尽管还有许多其他客户端用不同语言编写并由不同组织维护:

  • Geth:这是用 Go 语言编写的最受欢迎的客户端,因其简单性而广受欢迎。您可以使用它来下载主区块链,用 CPU 进行挖矿,部署合约并与之交互,创建以太坊账户等等,这些功能对一般以太坊开发人员非常有趣。它提供了一个命令行界面,您可以快速访问所有功能。

  • Parity:这是由以太坊的联合创始人之一创建的较大客户端,用 Rust 语言编写,与 C++ 语法类似,但在各方面都倾向于安全性。Parity 提供了一个命令行和 Web 界面,您可以使用它来创建账户,下载区块链,部署智能合约等等,Parity 以创建多重签名钱包而闻名,这是一个智能合约,帮助组织在处理资金和投票时做出决策,因为所有成员必须就每个决定达成一致意见。

或者,您还可以选择使用用 Python、C++ 和其他语言编写的客户端进行个人项目开发。选择权在您手中,所以请确保您对所有客户端进行详细了解,以更好地理解每个客户端可以为您提供什么功能。

挖矿层

在基础设施下方,您有挖矿层,可以使用您的显卡或 CPU 来挖取以太坊的各种实用工具。您可能知道,生成以太坊的唯一略微有利可图的方式是利用廉价的电力和 GPU 卡。实际上,您可以自己进行挖矿,或加入支付部分区块奖励的矿池。

以下图表展示了挖矿层包含的内容:

挖矿是通过参与挖矿的一组计算机通过工作量证明方案达成共识,增加包含有效交易的区块来增加区块链的大小的过程。这是为了进行加密货币转账以及能够利用挖矿网络的综合力量来运行智能合约而至关重要的。

以太坊的挖矿网络是巨大的,它负责处理发生的所有交易,包括智能合约的执行和部署。这是任何强大区块链的基础。您拥有的挖矿者越多,整个系统就越难以审查和更加安全。

当涉及到挖矿时,您有挖矿矿池和使用所有可用图形卡来解决工作量证明PoW)挑战的挖矿客户端。挖矿矿池将向所有参与者提供他们从独自挖矿中获得的一部分区块奖励,因为挖矿已经变得指数级困难,只有许多计算机的联合努力才能稳定地生成区块。它们非常适合获得持续的奖励,而不必花费数天的时间独自挖矿,不知道自己是否能找到一个区块:

  • Ethermine:在许多其他类似的矿池中,Ethermine 以其丰厚的支付和稳定的回报而闻名。作为您将找到的最大的矿池之一,您有很大的保证,在达到最低 0.05 ETH 后,您将收到您挖矿的 Ether。

  • Nicehash:这是一个特殊的矿池,因为它与更多种类的加密货币一起运作。它更像是一个以比特币为交换媒介,以更大的支付额度出售哈希算力的市场,您可以轻松转换为 ETH。支付门槛为 0.001 BTC,这比 Ethermine 要求的要昂贵得多。它非常适合那些考虑长期利益的人。

在选择挖矿矿池之后,您需要获取一个挖矿软件,该软件将以尽可能高效的方式利用您的计算机资源,以获取最大的支付额。最受欢迎的GPU矿工是 Claymore 和 Ethminer,虽然还有许多其他矿工,如 Geth 和 Parity,也可以用于相同的目的,但其 CPU 的哈希输出要低得多:

  • Claymore:这是一个出色的工具,可以充分利用您的 Nvidia 和 AMD GPU 的性能,因为它针对这些卡的各种类型进行了优化。包括双挖矿支持,您可以使用最小的影响来挖矿 Ether 和您选择的其他加密货币,因为它们使用相似且互补的算法,可以很好地协同工作。这个矿工的唯一问题是,使用它挖矿的每个小时,将有 36 秒用于为应用程序的开发者挖矿,这对于提供比其他矿工更好性能的如此有效的工具来说是可以理解和公平的。

  • Ethminer:Ethminer 是用 C++编写的,专门用于挖掘使用哈希算法 Ethash 的任何加密货币,其中包括以太坊、以太经典和 Musicoin 等。它不像 Claymore 那样高效,但是背后有一个强大的开源社区,该社区不知疲倦地努力提供最佳支持和编程,这是一支由热情的开发人员组成的小团队。

理想情况下,您应该测试这两个矿工,看看哪一个能够产生最佳性能,因为每个设置和计算机略有不同。如果您的性能不佳,请确保调整它们提供的设置。

点对点层

这是以太坊必须正常运行的最低层。P2P 层负责通过消息以实时方式直接与节点通信,而无需通过中间人。

以下图表显示了 P2P 层包含的内容:

P2P 网络中的每个节点都是可发现的,因为具有唯一的 IP 标识符和包含有关连接的每个节点的数据的哈希表。例如,当您连接到节点 123.456.123 时,您可以发送请求来查看它连接的节点,从而为您提供在未来可以连接的已知节点范围的选项。这对于能够在需要时发现节点并连接到新对等方至关重要。您连接的节点越多,您获取重要消息的通信速度就会越快。

了解 DEVP2P

DEVP2P 是一种网络协议,负责在节点希望通过发送消息进行交互时管理协商。每个消息都使用一个名为RLPx的协议进行加密,这允许节点在网络上交换加密数据,并确保除接收方外,没有人能够解码消息。

要使节点彼此连接,它们会暴露任何要使用的 TCP 端口,该端口默认为 30303。因此,如果您想连接到特定节点,必须确切地知道它使用的端口、它使用的协议以及其 IP 地址或唯一标识符。例如,我们之前讨论的消息传递协议 Whisper 使用shh协议,而以太坊使用eth协议。这只是一个简单的区别,当您使用 web3.js 时就会显现出来,因为它显示了每个对应协议中的每个函数。

总之,您已经了解了以太坊架构的哪些组件,因此在开发智能合约时能够辨别出您实际使用的部分。您应该能够识别网络中使用的不同协议,并了解您可用的工具。

在利用手头的工具方面,您刚刚提高了意识,这对于所有想要实现经过时间考验的解决方案以解决常见问题的以太坊开发人员都是至关重要的。

区块链共识

以太坊区块链正在一个网络中运作,该网络由成千上万台相互连接的计算机组成,交换信息。现在,为了确保他们都处理相同的交易并为了实现相同的目标作为集体努力的一部分,我们需要一些达成协议的手段,这就是所谓的共识,以便他们都同意结果。这是将努力结合起来以使整体受益的最佳方式。

在下面的图表中,您可以看到所有计算机都在运行相同的挖矿软件,并且一致同意单一协议,为他们的计算能力提供奖励:

引入区块链共识

与标准的投票过程相比,在那里所有个体独立决定他们想要实现的目标,区块链中的共识更进一步,旨在做出有助于以最有效方式达到最终目标的决定。例如,桥梁建设项目的管理者做出的决定基于共识,以达成建造桥梁在最有效的时间和成本方式中的最佳解决方案。当涉及到区块链时,共识是矿工们采取的一系列行动,以最有效的方式生成尽可能好的区块,从而减少区块时间,同时矿工们获得有利可图的以太币奖励。

PoW 是用来处理交易并生成包含交易的区块的方案,以换取一项困难的任务,以便只有最有能力的计算机获得区块生成奖励。当矿工生成交易时,他们所做的就是生成随机哈希直到找到适合下一个区块的哈希。每台计算机都是独立进行这样的操作。当矿工找到正确的哈希时,他们将生成该哈希的材料分发给所有连接的节点,以便它们也能做同样的操作,并最终将这个新区块添加到网络中,同时因努力获得奖励。

为什么所有矿工都同意发现正确哈希的人应该获得下一个区块的奖励?因为,当他们进行挖矿时,他们是在遵守一套规则。

这些规则如下:

  • 发现区块的用户将获得奖励。

  • 作为矿工,您必须将这个新区块分发给所有连接的节点,以同步整个网络中包含在该区块中的新交易。

  • 您必须只向区块添加有效的交易。

在这一点上,您可能会问自己当无效交易被添加到一个区块中时会发生什么? 答案是,每当矿工向区块添加无效交易,解决了工作证明挑战并试图将其分发给其他人以更新主链时,其他矿工将注意到这些无效交易,并停止分发该区块,因此它将不会进一步发展,因为他们中的每个人都是对每个区块中发生的事情的验证者。

解决包含这些无效交易的区块的 PoW 挑战的矿工浪费了他的计算能力,因为它不会进一步传播。 这是对恶意行为的惩罚,它是能源和时间的浪费。

这是一种非常强大而简单的检查机制,它保护区块链免受恶意意图,从而只批准已成功验证的有效交易。

展望未来

以太坊核心开发人员正在努力开发下一个版本的以太坊共识方案,该方案采用了股权证明PoS)的方案,该方案将在不久的将来的一个硬分叉中命名为Serenity,约在一到两年内发布。Ethash 不再用作实现和处理 PoW 共识的算法,以太坊将使用名为Casper的东西作为共识的协议,结合 PoS 和 PoW。

为什么它们正在改变生成区块和达成共识的协议?因为 PoW 在资源上不可持续,因为它迫使矿工浪费无法支撑的能源和资源。 没有必要在加密挑战中浪费所有这些能源。 我们可以通过不需要太多能源的简化协议达到相同的目标。 而且,最大的加密组织能够负担得起大量的计算能力,使得少数公司成为几乎整个矿业网络的所有者。 这是一个严重的问题,因为它破坏了去中心化的原则。 如果他们可以控制整个网络,那么该系统就可以被视为中心化,因此他们获得了更强大的立场来决定加密货币的发展方式。

理论上,他们可以合并努力并发动 51%的攻击,自由操纵交易。

卡斯珀计划通过使用 PoS 算法来解决 PoW 意外增长所带来的问题。 然后,矿工在网络上锁定特定数量的以太币,以便能够验证交易,同时通过交易费用获得报酬,因为新的区块创建将停止获得奖励。 每个矿工都会为他们认为合法的区块押注任意数量的以太币,如果他们是正确的,他们将获得他们押注的一部分。 这使得整个网络更加去中心化,因为更多的计算机能够参与共识过程,以加速并从少数大玩家手中释放资源。

在以下图表中,您可以看到如何通过对随机生成的区块提出下注来生成区块的可视化表示:

在 Casper 中,将会有一种特殊类型的矿工,被称为forger,他们将通过合并交易并在过程中添加他们的公钥来生成区块,以证明他们是创建该区块的人。如果他们最终将正确的区块添加到区块链上,他们还将获得费用奖励。那些对正确的区块下注的人将被称为validators。每当一个区块有足够的下注,并且如果它在标准区块链的标准区块时间内生成,即该区块将被添加。forger 将从所有 validators 中随机选择,以便少数人能够创建区块。

无利可图问题

当使用 PoS 的 Casper 实施时,参与者将能够通过对他们认为有效的区块下注来推动区块前进,并在选择有效区块时获得奖励。但是,如果他们决定对恶意区块进行投票会发生什么呢?当对恶意区块下注时,他们不会损失任何东西,因此没有任何事情能阻止他们这样做。这就是无利可图问题所在。

这些矿工将能够对他们看到的每个区块进行押注,以获取可能的奖励。

解决此问题的一个可能方案是惩罚那些不断对错误区块下注的人。因此,如果一个矿工犯了 10 个错误的决定,他将损失一部分他的股份。但是,只有当网络的大多数决定投票给错误的区块时,这种情况才会发生,如果矿工分布足够广泛,很难就错误的选择达成一致意见。

区块链经济

加密货币的有趣之处在于它们将技术与在现实世界中具有价值的真实资产结合起来。历史上,我们首次能够创建完全自主运行的数字货币。从历史上看,货币始终由中央政府支持,他们控制着供应和需求。他们是唯一能够印刷货币以控制通货膨胀的人。

了解通货膨胀

简单来说,通货膨胀是经济中所有资产价格上涨的现象。例如,1890 年的一座房屋通常价格约为 10000 美元,但同一座房屋现在的价格约为 350000 美元,即使它变老了——这是高通货膨胀的结果。这可能是由于自然资源稀缺导致的材料成本增加。也许是因为工作机会不足,所以人们要求更高的工资,因为要求也在增加。也可能是因为土地成本增加,这是由于销售的房屋不足。所有这些以及许多其他因素都迫使企业提高价格,从而迫使人们在增加通货膨胀的同时赚更多的钱。

诸如以太坊之类的加密货币能够增值,因为随着技术不断改进和开发更多用例,采用率不断提高,这使得它们在一定程度上对外部通货膨胀具有一定的抵抗力。每年供应增长速度放缓的事实使价值相对于法定货币保持相对稳定,而政府和机构能够按需印刷法定货币,这使得以太坊和许多其他加密货币成为很好的价值存储工具。

评估加密货币经济学

询问加密货币为何具有价值等同于询问任何事物为何具有价值。我们对事物进行数值估值是因为它给了我们一些东西——一个提供给我们安全睡眠场所的房子,一个给了我们能力去远方旅行的汽车,或者提供生存所需的食物。一般来说,区块链作为一种工具具有实用价值,用于存储货币,进行几乎即时的数字货币转账,并使用一种特定类型的应用程序,如果没有区块链就无法工作。

在转换加密货币方面,交易所起着很大的作用。您可以前往接受以太币的任何交易所,并将其转换为任何其他货币,因为他们同意这是一种有价值的东西。货币基于协议。我们都同意苹果有一个特定的美元价值,这可能略有变化,只要人们想要消费这个苹果,并接受美元作为交换,他们就会购买它。以太币也是如此——只要人们想要以太币用于他们能做的任何事情,并接受美元作为交换,他们就会购买它。

在货币出现之前,人们通过协议交换物品,例如,一张桌子换一袋土豆。后来,他们决定使用一个抽象概念,即一个数字,可以更精确地理解物品的价值。

请注意,价格永远不是固定的。这不像米制系统,其中 1 公斤对于整个地球来说都是一样的。法定货币和加密货币通过与其他货币比较来确定其价格。1 美元对比 0.8 欧元,就是 1 美元对比 1 美元。1 以太币对比 200 美元。世界上的法定货币全球交易称为外汇(外汇交易)。对于加密货币,有个别交易所分别处理价格。

当有人愿意用一种币换另一种币的时候,两种币都会得到一个价格。例如,想象一下以下情景:

  1. 想象一下,有人刚刚创建了一种名为 RED 的新虚拟货币。它没有任何价值,因为还没有人想购买它,因为它还不为人所知。

  2. 现在,约翰读到了这个新的 RED 币,并决定它将帮助他以较低的价格购买苹果,因为他们已经与当地的生产商达成协议。RED 币具有实用价值。

  3. 他去了交易所,并与该币的所有者谈判。约翰想要购买 100 个 RED 币,所以他们谈论到他们决定 100 个 RED 币价值 300 美元,基于该币的实用性。

  4. 在那一刻,RED 币的价值为 3 美元,直到该币的创建者决定降低价格以增加买家或反之。

市场决定了所有货币的价格,这取决于人们购买它们的意愿。

确定加密货币的价值

任何事物的价值取决于供应和需求。只要供应保持一致,人们越想要它,它就会变得越昂贵。

在下图中,你可以看到供需如何影响特定加密货币与稳定的法定货币对应的价格:

许多交易所都有自己的流动性池,以太币在这些交易所的价值不同,因为人们基于偏好使用不同的交易所。那么,他们是如何同步使得所有交易所都具有标准价格的?事实上,它们仍然有稍微不同的价格。套利者致力于保持价格一致;因为他们的收入是基于从一个交易所到另一个交易所的价格差异,这是导致每种加密货币价格总体稳定的原因,尤其是像以太坊这样的大型加密货币,其中每日交易量足够大,值得投资。

随时间的推移,价格变动的结果可以在下一节的图表中看到,在那里,你可以快速分析特定历史事件时加密货币的价值。图表为区块链采用提供了很好的理解。

基于以太坊的代币

以太坊区块链如此著名的原因之一是因为它为创建代币提供了简单的方式。每个有大量追随者的代币都会不可避免地影响价格,因为背后的技术仍然是相同的。在接下来的章节中,你将学习到如何创建 ERC20 代币。目前,你只需要理解,每当有人投资购买代币时,代币的价格就会上涨,同时也增加了以太坊的总价值。

以太坊上的代币数量越多,区块链的价值就越高,因此它的价格就会上涨。如果你去coinmarketcap.com/,你会看到所有大型以太坊代币的价格,你会意识到大多数代币的价格模式与以太坊相同,原因很简单,因为购买代币的人实际上是在投资底层技术,即以太坊。

投资到以太坊的总金额被称为市值,因为以太坊的市值如此之大,只有大额以太币的购买才能明显影响价格。因此,每当你看到价格出现巨大波动时,很可能是因为一家大公司投资于技术出于他们自己的动机。

区块链指标

区块链指标是区块链的重要可衡量特征,可以帮助您了解其状态,以便确定以太坊区块链的表现如何,采用率如何,以便您在定价、发展和采用率方面预测未来。

交易数量

区块链中的交易数量是最重要的指标,因为它显示了技术在日常情况下的使用情况。交易代表活动。交易越多,越多的人积极使用区块链投资项目、运行 dApps 和使用智能合约。

如果没有人使用技术,拥有数十亿用户是没有意义的;交易越多,技术就越有用。您还将在下一个标题中看到,以太坊正在经历成长的烦恼,使用率不如预期。人们加入了它,但他们没有充分使用它。这是社区中一个必须克服的巨大问题,以成为主流。

当以太坊开始时,有 8,893 笔交易,然后在 2019 年 1 月 1 日达到每日 448,168 笔交易。2018 年 1 月 4 日的交易记录为 1,349,890 笔。

以下是以太坊自 2015 年推出以来交易数量的可视化参考:

前面的截图告诉我们,每天有 448,000 笔交易来自每日转账、使用 dApps 的人、参与 ICO 的投资者和与智能合约交互的人。相比之下,截至 2017 年 12 月,PayPal 每年处理 76 亿笔交易,约为每天 6.33 亿笔。PayPal 是全球广泛使用的主流技术,因此,为了让以太坊达到这种大规模采用的水平,它必须增长大约 1413 倍才能达到当前交易水平。这清楚地表明以太坊仍处于起步阶段,需要几年时间才能在这样的规模上变得相关。

图表显示,自去年以来,以太坊的每日交易数量没有增加。这在区块链领域是正常的,预计在一段时间后,由于技术尚未经历的许多改进,它将继续增长。

您可以通过查看以太坊交易图表 etherscan.io/chart/tx 自行探索数据。

地址增长

以太坊中的地址类似于现实生活中的账户,尽管更容易生成,因此人们通常会在任何给定时间管理 10 个地址并不奇怪。以太坊成立时总共创建了 9205 个地址。从那时起,以太坊没有停止增长,截至 2019 年 1 月 1 日,已创建了 54,281,633 个地址,每天增长约 51,139 个。这些是 5000 万个地址,我们可以将其近似为 500 万用户,因为我们可以假设普通用户具有约 10 个地址,考虑到创建它们的便利性。

这是以太坊地址总数多年来的增长情况:

如果您想探索有关以太坊地址的更多过去历史数据,可以查看etherscan.io/chart/address上的图表。

5400 万个地址向我们表明以太坊仍处于采用阶段,而且在不断增长。人们第一次发现这项技术,他们加入了,他们也带来了更多人。当我们达到 10 亿个地址时,我们可以自信地说这项技术已经成为主流,人们已经把它融入到他们的日常生活中。

以太市值

每个以太的价格向我们表明人们对技术有多少信任。有句谚语说,“言之有物”,这个统计数据非常贴切。人们在技术上投入的资金越多,他们对它的信任就越深,这表明扩张。在 ICO、TGE 和 STO 中筹集的资金也是以太价格上涨的良好来源,因为许多项目都成功地使用了这项技术。人们把钱投入到基于以太坊的有用项目中。

在下图中,您可以看到以太坊的价格随着时间的推移而波动,出现了几次突然的暴涨,这是由人们对这项技术的兴趣所引起的。

以太坊是在 2015 年诞生的,当时 1 个 ETH 价值 0.31 美元。两年后,到 2017 年 12 月底,1 个 ETH 的价值超过了 1000 美元。最近,价格下跌,截至 2019 年 1 月 1 日约为 142 美元。这向我们表明,人们在逃离它,因为他们受骗了,因为他们对它没有用处,或者只是因为炒作结束了。真正的原因不确定,但可以肯定的是,价格下跌是因为这项技术需要提供创新的解决方案来解决只能通过区块链解决的现实问题。

如果想了解有关价格如何随时间变化的更多信息,请查看 Etherscan 上的图表etherscan.io/chart/marketcap

开发者采用

一个项目没有背后的人改进和发展到新的领域就什么都不是。以太坊背后的开发者的数量和质量是未来发展的指标。越多优秀的人参与其中,它就越快能够面对可能解决人们问题的新挑战,提高实用性。我们可以通过使用以太坊发布的 dApps 数量和 ERC20 代币的数量来衡量开发者采用程度。

区块浏览器 Etherscan 表示,截至 2019 年 1 月 24 日,在以太坊区块链上发布了总共 164,188 个代币合约。您必须考虑到其中许多是欺诈行为。大约有 1,000 个是真实的,考虑到每个好的代币,都有 10 个假的或被放弃的代币。这个数字告诉我们,以太坊在开发者采用方面是主导的区块链,因为其他区块链很难达到几百个代币。人们正在加入网络并了解更多有用信息,因为代币背后有有用的应用。

状态 dApps 是最受欢迎的网站,汇集了大多数构建在以太坊上的应用程序。据说这个区块链上有 2,432 个 dApps。这比任何其他区块链都多得多。然而,我们必须将其与实际场景进行比较,因为以太坊主要是用于构建不可阻止的应用程序的协议。例如,Google Play 列出了约 2.6 百万个活跃应用程序,这比部署在以太坊上的应用程序数量大了 1,000 倍。

根据这些数据,我们可以得出结论,以太坊又一次处于增长阶段,采用刚刚开始,因此我们必须继续在其上构建,直到人们决定参与解决去中心化的问题。

要了解更多信息,您可以查看 etherscan.io/tokenswww.statista.com/statistics/266210/number-of-available-applications-in-the-google-play-store/

矿工活动

最后,矿工活动是技术背后力量的重要衡量标准。越多的矿工加入网络,它就会变得越强大,进而增加其去中心化程度,达到创纪录的高算力。

以太坊在 2015 年开始时的算力仅为 11 GH/s。这是极低的,而且是任何新的区块链都可以预期的。然而,这个数字并没有停止增长,现在截至 2019 年 1 月 1 日,我们大约有 180,000 GH/s。

看一看以太坊的网络算力如何持续增长的截图,以清楚地欣赏价格与采用之间的关系:

您可以从前面的图表中看到,算力一直保持稳步增长,直到 2018 年 9 月份下跌,随后缓慢回升。通过解读这些数据,我们可以得出结论,慢慢被相信这项技术的矿工们所接受,特别是最近一段时间,难度增加而奖励减少。这种加密寒冬仍然异常强劲,得到愿意承担损失挖矿的人的支持。这是人们对底层系统有多么信任的一个很好的指标。

一定要查看官方图表,自己分析这个有趣区块链的算力,以发现有趣的时间点,人们在这些时间点决定参与挖矿过程,网址是etherscan.io/chart/hashrate

总结

在本章中,我们了解了与将区块链技术与现实场景集成的机制相关的许多事项,例如交易所和采用率。您现在能够使用强大的工具来改善您的去中心化应用程序和智能合约的质量,同时还能够根据以太坊的最重要指标预测未来价格走势。在第一节中,您看到以太坊区块链是如何使用一组特定规则(称为链规范)来定义的,然后学习了使这项技术如此引人注目的不同组成部分,包括构建在每个层级上的许多应用程序。最后,您了解了以太坊共识、经济和度量是如何工作的,以帮助您了解为什么以太坊是如此有价值的数字货币,同时看到了许多节点如何共同合作以实现一个目标,使用所谓的共识。

在接下来的章节中,我们将介绍以太坊协议的不同实现,以便能够在您自己的项目中充分利用以太坊的潜力。

第三章:以太坊资产

在本章中,您将了解不同的以太坊协议实现,以便更好地理解区块链可以以许多不同方式集成。在学习协议实现之后,我们将继续探讨 INFURA、钱包以及去中心化自治组织—也被称为DAOs—这样您将学会如何在区块链上创建不受中央政府限制的商业网络。我们将讨论几个区块浏览器,以便学习如何检查您的交易,从而分析在上传智能合约到区块链时发生的情况。最后,您将了解有关区块链实施的一些重要问题,以帮助您解决您可能在某个时候面临的常见问题。

在本章中,我们将涵盖以下主题:

  • 以太坊协议实现

  • INFURA 要点

  • 去中心化自治组织

  • 杂项与关注点

  • 创建您自己的私人以太坊网络

  • 选择以太坊钱包

  • 使用 Etherscan 进行交易

  • 创建账户

以太坊协议实现

以太坊是一个开源系统,可以用不同的语言实现,以便为人们提供使用他们喜欢的工具进行交互的选择。其中一些完整节点的实现是 Geth 和 Parity。我们将在本节中探讨它们的区别,以及在什么情况下使用它们是最佳的场景。

协议实现

正如您在前面的段落中所读到的,我们将涵盖的以太坊实现包括 Geth、Parity、Mist 和 Embark。为什么只有这四个?因为它们是最流行的实现,使您能够完全执行区块链的所有功能。挖矿、进行交易、下载整个区块链、与您部署的合约互动以及创建账户这类事情在所有这些实现中都是可能的。

理解 Geth

Geth,也被称为 Go Ethereum,是最流行的以太坊实现,可以让您执行各种任务。我们将在接下来的章节中详细介绍如何执行大部分任务,帮助您理解每个任务所需要的每个命令。

您可以在 macOS 上轻松安装 Geth,使用以下命令:

brew update
brew install ethereum
brew upgrade ethereum

或者,如果您使用的是 Windows 或 Linux,您可以前往geth.ethereum.org/downloads/,然后获取适用于您系统的可执行文件。无论哪种情况,geth 命令将在您的终端或命令行中安装后可用。

创建账户

您可以使用 Geth 创建无限账户,因为它们是由称为Rivest Shamir AdlemanRSA)的算法生成的,该算法在链下创建公钥和私钥。可能的组合数量如此之大,几乎不可能生成与另一用户相同的账户。要使用 Geth 生成一个账户,请运行以下命令:

$ geth account new

这将询问您一个密码,以保护您的凭据安全。然后,您可以使用以下代码管理您的帐户:

$ geth account list
$ geth account update <your-accounts-address>

update 命令用于更改特定帐户的密码,以便您可以轻松解锁它。您可以使用此命令删除现有密码。

您还可以使用以下命令解锁您的 Geth 帐户:

$ geth --unlock <your-accounts-address> --password <your-password> 

这基本上是解锁您的 Geth 帐户,以便您可以通过控制台直接发送交易。

请注意,您也可以在被询问时按下 Enter 键而不输入任何内容即可创建无密码的帐户。

下载区块链

Geth 的主要功能是下载区块链,以便您可以在设备上拥有一个完整的节点,以满足您可能需要的任何目的。要执行此操作,请在终端上执行以下命令:

$ geth

在没有参数的情况下运行 geth 会自动开始下载最新版本的区块链,并将其下载到默认目录,RAM 默认大小约为 1 MB。您可以使用此标志更改下载区块链的位置:

$ geth --datadir "<your-location>"

要更改用于下载区块链的 RAM 部分,请使用此标志:

$ geth --cache=1024

此标志中的数字表示您想要专门为此进程分配多少 RAM。您分配的越多,它下载主以太坊链的每个组件的速度就越快。尽管如此,由于链中包含了大量的过去交易信息,获取完整的数据链可能需要数小时。请记住,链会永久存储每笔交易,没有删除过去交易的选项。

然后,您可以使用以下命令在下载链后可视化节点指标:

$ geth monitor

使用您的 CPU 进行挖矿

Geth 也能够利用您的 CPU 独立挖掘以太币,但与使用图形卡或特定应用集成电路ASICs)等替代方法相比,这不是一种高效的过程。尽管如此,将其添加到此工具中可能在需要低资源挖掘的不同情况下很有用,比如在物联网设备中。

要使用 Geth 进行挖矿,请执行以下命令:

$ geth --mine

如果尚未同步,则它将开始下载和同步您的链。在设置了您的 etherbase 后,您将能够接收资金,这可以轻松地完成如下:

$ geth --etherbase 0

最后一个数字表示将从您的列表中使用哪个帐户。您可以使用 geth account list 命令查看您的帐户。

Parity

Parity 是最发达的以太坊协议实现之一,因为它为您提供了一个令人惊叹的界面,您可以从中使用所有功能,包括部署合同和与区块链交互。

要安装 Parity,请在 Linux 或 Mac 上运行以下命令:

bash <(curl https://get.parity.io -L)

它将自动下载最新版本,编译它,并立即获取所需的组件以使其正常工作。不过,对于 Windows,您需要从它们的官方网站获取可执行文件,网址是:www.parity.io/ethereum/

下载区块链

在能够与 Parity 交互之前,您必须下载区块链,这可以通过执行以下命令简单完成:

$ parity

安装完成后,您将能够访问 localhost:8080 网站上的 Web 界面,该界面将显示您所有可用的选项。

创建账户

要使用 Parity 创建账户,您只需点击 ACCOUNTS 选项卡,然后在主选项卡下方选择 + ACCOUNT,以生成一个新账户,如下图所示:

使用 CPU 进行挖矿

Parity 可以与其他 GPU 矿工配合使用,这将为您提供更好的哈希速率,以便您可以从一开始就变得有利可图,与使用 CPU 进行挖矿相比。然而,您可以通过运行以下命令来使用您的处理器进行挖矿:

$ parity --author <your-ethereum-address-without-0x>

然后您将开始向该地址生成微小量的以太币。

每种协议的最佳方案

Geth 和 Parity 都是出色的工具,可以为各种分散式应用程序和智能合约开发框架执行相同的工作。然而,有特定的用例可以让其中一个比另一个更好。

智能合约开发

并且在下载完整的以太坊主链之后,它们都提供了一种简单的部署和与智能合约交互的方式。然而,鉴于 Parity 在易于使用的 IDE 中为您提供了一个专用选项卡来编写智能合约代码,因此 Parity 是更好的选择。您可以将智能合约编译到特定版本,保存它们,并将它们部署到网络上。更好的是,它会自动突出显示您的代码,这在大多数 Solidity 和 Vyper 编辑器中都很少见。

另一方面,Geth 使用命令行界面,这使得智能合约开发变得更加困难。

就我个人而言,我发现 Parity 的界面是任何类型的以太坊开发人员的绝佳工具。它快速,外观出色,您完全控制您自己的节点,感觉良好,而且易于使用。

管理账户

在管理多个以太坊账户时,您可以使用 Parity 提供的 Web 界面或使用 Geth 提供的命令行工具。在这种情况下,两者都是不同用途的绝佳选择:

  • 如果您正在管理多重签名钱包,请使用 Parity,因为它具有内置的部分来管理这些类型的复杂钱包,而这在 Geth 中是缺失的。

  • 如果您需要一个基于命令行界面的简化系统,该系统为您提供更少的头疼以进行更快的操作,那么请使用 Geth,鉴于您不需要打开 Web 界面与其进行交互。您不必等待此类 UI 显示的小但明显的加载时间。

其他用途

当涉及到不同的情况,比如挖矿、下载区块链、转账资金以及探索你本地区块链的历史时,决定哪个选项更好取决于你。两者都是相等的,都会给你带来相同的结果,只是有些微小的差异。

你已经了解了这些以太坊实现为你可以做的主要特性。我们看到了 Geth 和 Parity 如何被使用,他们如何与区块链交互,以及一些你可以直接应用于自己的个人项目的真实世界命令。记得去看一下 Parity 的神奇智能合约代码编辑器:你会喜欢的。

INFURA 要点

INFURA 是一个流行的技术,可以连接到区块链而无需下载它包含的数百吉字节,因为它免费提供给你完整的节点。

理解 INFURA

INFURA 通过为用户提供可用的以太坊实例来连接到区块链,帮助数百万用户连接到区块链。这是一个旨在改变以太坊采用率的最大项目之一,因为每个人都能够立即连接到以太坊,以处理交易并与区块链进行交互。

它支持像 MetaMask 这样的流行应用程序,让你可以从浏览器进行交易。这就是 ICO 变得如此流行的原因之一——因为人们发现了一个简单的方法来与区块链进行交互,以便使用 MetaMask 和 INFURA 的基础设施轻松执行交易。你可以自由地在自己的应用程序中使用 INFURA,只需使用适合你项目的正确网络的链接:

现在,这些是免费的网址,但在你可以发出的请求数量上有限制。如果你想无限制地访问区块链,你将需要在infura.io上创建一个账户,你将会得到一个唯一的令牌 ID,你可以在自己的需求上无限制地使用它。

INFURA 也可以与 IPFS 一起使用,IPFS 是一种去中心化存储的协议,类似于 torrent,因此你可以在去中心化网络中上传内容而不需要中心化。

INFURA 的内部运作

表面上,INFURA 提供了一个干净的 API,许多区块链开发人员可以自由使用它们的分散式应用程序,以提高以太坊的采用率。

在幕后,他们有一组计算机,上面安装了最新版本的以太坊区块链,并且同步地动态调整资源需求,每当用户产生更大量的交易时。以下图表展示了它是如何工作的:

与自己下载最新链时耗费大量千兆字节和小时的时间相比,你可以简单地连接到 INFURA,这样你就可以立即访问各种以太坊链而无需任何限制。

使用 INFURA 的仪表板

当你在 INFURA 注册为新用户时,你将可以访问仪表板,你可以在其中创建项目,查看有关项目 ID 的统计信息和详细信息:

第一件你会想要做的事情是点击“创建新项目”按钮,创建一个新项目,以便你的 dApps 能够连接到正确的区块链端点,无论是 Ropsten、主网、Rinkeby 还是 Kovan。一旦你点击了那个按钮,你将被要求命名它,然后项目将被创建:

如果你点击“查看项目”,你将看到一个页面,其中有修改该端点安全性和名称的设置,以及删除它的选项:

如果你点击“查看统计”,你将看到一个页面,其中包含大量关于请求数量和带宽使用情况的信息,以及关于端点使用时间的统计信息,以及其他有价值的信息:

除了那些按钮,你还有一些可点击的链接,你可以快速访问文档和 INFURA 的状态,并在需要时获得支持:

INFURA 的基础设施确实是一个有趣的命题,由 ConsenSys 的人们制作,他们创建了一个庞大的连接用户的网络,希望参与区块链革命,而不需要下载如此庞大的区块链所需的时间和存储。

这是向所有即将利用这一激动人心的技术的新用户提供开放访问的最佳方式之一。它也完全符合开放技术的区块链核心概念。然而,如果太多的应用程序和用户最终依赖于它,使其成为一个中心化的故障点,那么如果服务失败,它可能会导致数千个 dApps 无法运行,从而成为未来的一个问题。为了避免这样一个悲剧性的场景,请记住为生产应用程序设置自己的以太坊节点,以增加去中心化。

去中心化自治组织

去中心化自治组织,也被称为DAOs,是区块链的最佳用例之一,因为它们是信任和应该更新为现代时代的旧系统之间的完美链接。

解释 DAOs

DAO 的目标是成为企业做出决策的主要场所。它们是传统公司的去中心化版本,通过与利益相关者和每个 DAO 的关键组成部分讨论来做出决策。简单来说,它们利用了以太坊区块链的不可信任性来形成具有所有可能性的法律公司。

他们依靠智能合约提供硬编码功能,用户可以通过多重签名钱包做出决策并达成协议。多重签名钱包类似于以太坊账户,因为它们存储 ETH 并能够进行转账;然而,底层技术由智能合约组成,强制成员通过使用自己的地址签署每个决定来达成一致。

它们被称为 自治,因为它们在区块链顶部自行运行,无需任何人维护它们,与传统公司相比,传统公司必须研究每个组成部分的发展以避免法律后果和结构问题。

在 DAO 内操作

创建不受政府或外部实体控制的不可阻挡组织的想法比起初看起来要简单得多。这就是为什么我准备了一个简单的图表,展示 DAO 在日常基础上参与的流程:

每个 DAO 都是独立运作的,每个用户都能创建要投票决定的议题。每个代币持有者都可以选择以取决于他们拥有多少代币的权重进行投票。你拥有的总代币比例越大,你在组织中获得的决策权就越大。

最初,这不应该是一个问题,因为人们预计会投票选出最好的公司结果,但后来可能会导致集中化问题,其中少数人会控制大部分代币,从而能够为个人利益合作。这类似于 51% 攻击,其中整个公司的一部分做出所有决定。

用于 DAO 的工具

你可以编写自己的 DAO 智能合约,为组织成员提供创建具有投票机制的提案的功能,但已经存在的解决方案可以帮助你简化流程,加快设置时间。

AragonOS 是用于创建去中心化自治组织的最常用的开发框架之一,它给你以下选项:

  • 将你的 DAO 智能合约更新到最新版本,并包含所有改进

  • 限制对具有权限控制的特定用户的访问

  • 在其之上添加额外的投票应用程序以扩展功能

这个想法很简单,只需创建 DAO,配置权限,并开始构建组织的组件,以达成治理决策。你还可以使用 AragonUI 以美观简洁的方式与你的组织进行交互。后来被黑客攻击的著名 ICO,即被称为The DAO的项目,由于产生了如此重大的后果,所以不再可用,因此你无法从它们为创建新的去中心化组织提供的选项中受益。

杂项和关注点

以太坊是一个复杂的工具系统,通过协同工作实现了一个授权解决方案,帮助成千上万的用户进行去中心化交易,他们无需信任外部实体来保护他们的个人数据和滥用规则。因此,对于这些内部部分如何相互通信的方式有疑问是完全正常的。在这里,我们将为您提供一些在开发和使用区块链过程中任何用户都会面临的最常见问题的答案,以便您能够将这些新知识与坚实的基础联系在一起。

理解以太坊改进提案

以太坊是一个不断发展的技术,不断改进,各种专家产生的新研究推动了去中心化技术的发展。新变化被纳入这个区块链的方式是通过以太坊改进提案,更为人熟知的是EIPs

标准如 ERC20 和 ERC721 是由 EIPs 提出的,并分别用数字 20 和 721 进行标识。这些 EIPs 是在官方以太坊 GitHub 存储库中以 EIPs 的名称创建的,位于 github.com/ethereum/EIPs/issues,在这里你可以看到人们通过改进为整个系统带来益处的核心以太坊创新。事实上,ERC20 标准是为了满足基于以太坊生成唯一加密货币的需要而创建的。

它们是以太坊演进的重要组成部分,并提供非常有趣的功能,使这个区块链成为解决人们问题的技术的最佳选择之一。

区分使用 Truffle 和集成开发环境(IDEs)的用法

你可能熟悉被称为Truffle的框架,它允许你以可持续的方式编写、部署和测试合约,一旦设置完成,就需要最小的配置。它是部署高级和简单项目的最佳工具之一;然而,对于特定用例,你可能不想使用它。

例如,你可能正在处理一个简单的 50 行智能合约,为你正在构建的应用运行一些基本功能。在这些你知道智能合约代码将很小的情况下,最好使用一个 IDE,它让你能够开发你的代码,而不必花费宝贵的时间来使用 Truffle 设置项目。有时,你只需要编写一个智能合约,因为你不打算通过新功能来升级它。在这种情况下,你应该选择诸如 Remix 之类的 IDE 来节省时间和简单性。

另一方面,当你在处理需要测试和可维护性的较大智能合约的复杂 dApp 时,应选择 Truffle,以确保安全性。

理解智能合约的局限性

当你打算在一个新的基于区块链的项目上工作时,你必须考虑到当前智能合约语言所面临的限制,因为它们将决定你在开发中能够达到多复杂的程度。记住,你的 dApp 的好坏取决于其背后的智能合约,所以考虑在智能合约中可以做什么和不能做什么是有意义的。

作为智能合约开发者,你将面临一些问题,因为你的程序由于无法创建无限大的智能合约而不断地缩小。你的合约的大小有一个固定的限制,所以你必须在这些限制内工作。

当创建复杂的智能合约时,要注意编译器给出的信息,以了解何时遇到障碍以及如何解决它。你将面临程序根本无法按照你想象的方式完成的情况。在这些情况下,你必须了解循环限制、函数成本和智能合约的互操作性,才能成功地创建更大的代码。

创建你自己的私有以太坊网络

作为区块链开发者,你总会在某个时候需要创建一个具有自定义属性的以太坊区块链。考虑到侧链开始出现,以及著名的扩展技术Plasma的出现,这变得更加重要了。

在 Mac 上设置私有区块链

那么,我们如何创建一个自定义的以太坊区块链呢?以下是适用于 Mac 的说明。我已经尝试过它们,应该也适用于你:

  1. 首先,通过执行以下命令使用homebrew安装以太坊包:
brew install Ethereum

这将需要一些时间来处理,特别是如果这是你第一次执行brew。在我的情况下,它显示了Updating Homebrew…,过了一段时间后,它就被安装了。

  1. 现在,你应该能够使用gethpuppeth命令。Geth是主要的命令行界面,用于与以太坊区块链进行交互,可以下载主链等任务。Puppeth是用于设置自己的私有区块链的工具。

  2. 在设置你的genesis.json文件之前,你需要一个以太坊地址。使用以下命令创建一个帐户:

geth –-datadir .ethereum/ account new
  1. 它会要求您输入密码。只需键入密码,您将看到您帐户的地址。确保将其复制到安全的位置。

  2. 之后,执行以下命令:

puppeth

这将产生以下输出:

  1. 它可能会要求您管理一个网络。在这种情况下,只需键入您想要创建的新网络的随机名称:

  1. 然后,选择第二个选项创建一个新的genesis文件:

  1. 从头开始创建一个新的genesis文件,并选择第一个共识引擎,proof-of-work

  2. 现在,它会问您哪些帐户应该被预先资助。在这里,您将粘贴之前使用geth命令生成新地址后复制的帐户地址:

  1. 然后,它会问您是否应该预先用1 wei资助预编译地址。回答yes。基本上,这是一堆将用于编译区块链的地址。之后,指定一个随机的网络 ID:在我的情况下,我选择了77作为 ID。以太坊主公共网络的 ID 为1,所以您必须选择一个不同的数字来区分您的区块链和他们的区块链。这很重要,以避免重放攻击,其中恶意用户可能会将一笔交易从一个区块链复制并在另一个区块链中执行以获取自己的利益。这个 ID 号码正是避免了这种漏洞:

  1. 您已配置并创建了将随时用于启动您自己的私有区块链的genesis.json文件。现在,您必须导出它以便能够使用它。要这样做,请选择已执行的puppeth命令的第二个选项——管理现有创世配置——然后选择导出创世配置

  1. 然后,它会问您要将创世文件导出到哪里:

  1. 您将在所选文件夹中看到您的文件。如果您没有选择一个,文件将被创建在您的用户文件夹中或者您的终端当前位置的任何地方。以下是您会找到的文件:
  • genesis.json:或者您为您的创世网络选择的任何名称。此文件包含我们之前看到的一般链规范。

  • genesis-aleth.json:这是用 C++编写的 Aleth 以太坊客户端的创世文件。Aleth 只是一个以太坊客户端,之前被称为 cpp-ethereum。

  • genesis-harmony.json:Harmony 是另一个独立的客户端,用 JavaScript 和 Java 编写。这是与其规范兼容的创世文件。

  • genesis-parity.json:Parity 是用 Rust 编写的著名的以太坊客户端。这是与 Parity 兼容的创世文件。

  1. 太棒了!你现在已经为你的私有以太坊区块链导出了创世文件。要初始化你的区块链,请执行以下命令:
geth --datadir .ethereum/ init <your-genesis-file-location>.json

这将加载创世区块链状态并启动你的私有以太坊区块链。确保在你的以太坊地址前面添加0x前缀,如果你还没有这样做。记住,这个地址是你以前使用geth命令创建的。它会询问你账户密码,这是你创建时设置的。输入密码并按Enter解锁。如果你忘记了密码或账户,你需要返回使用geth命令创建一个新账户,并在询问时使用该地址创建一个新创世文件。

就是这样!你现在拥有一个具有自定义配置参数的工作私有以太坊区块链。这个私有区块链有趣的地方在于你可以将其连接到 Remix IDE。这将使你能够以极快的速度部署和测试合约。要能够将你的私有区块链连接到 remix IDE,你必须允许 remix 域在你允许域名列表中。只需执行此修改后的命令启动区块链:

geth --nodiscover --datadir .ethereum/ --unlock 0x<YOUR-ETHEREUM-ADDRESS> --mine --rpc --rpcapi eth,net,web3 --rpccorsdomain https://remix.ethereum.org

使用我的以太坊地址,看起来像这样:

geth --nodiscover --networkid 77 --datadir .ethereum/ --unlock 0xf30c37b1e5ed82eebd1a7cf4c66cb9497faa4799 --mine --rpc --rpcapi eth,net,web3 --rpccorsdomain https://remix.ethereum.org

现在,前往 remix.ethereum.org。注意,域名必须以https开头,并以 s 结尾以增加安全性。一旦到达那里,选择运行选项卡,并将环境更改为 Web3 提供程序:

它会询问你是否确定要连接到以太坊节点。点击确定并再次点击确定,以确认 Web3 提供端点而不进行修改:

这样你就可以访问在你的私有以太坊实例上运行的帐户,以便你可以自由部署和测试智能合约。

在 Windows 上设置私有区块链

我决定忽略 Linux 和其他操作系统,因为事实上 Windows 和 Mac 是目前开发中使用最广泛的平台。因此,在这一部分,你将学习如何在 Windows 上创建一个私有区块链。步骤几乎相同,但需要进行一些微小的调整,以便清晰指导你如何构建这种自定义私有区块链。

你已经了解到构建一个私有以太坊区块链是有用的,因为可以加快开发时间,可以配置区块链以符合自己的偏好,快速的区块时间,并且可以获取所需的知识,随时启动自己的以太坊实例。通过以下步骤,你将学习如何在 Windows 上设置私有区块链,这是你需要掌握的用于更大项目的技能:

  1. 第一步是下载 Geth。Geth 是主要的以太坊客户端,包含了运行自定义区块链所需的逻辑。你可以通过访问geth.ethereum.org/downloads来下载它:

  1. 点击 Geth for Windows 按钮开始下载。运行安装程序时,会询问是否要安装开发工具:

  1. 一定要激活它。选择前述选项因为你将使用所需工具来设置你的自定义区块链。然后,只需点击下一步和安装完成安装。如果成功,你将能够在命令行或 Windows PowerShell 上运行geth,我推荐使用 Windows PowerShell,因为它包含了额外的命令并看起来更专业:

  1. 如果你运行geth命令时无法执行Ctrl + C来避免从以太坊主网络上下载数百千兆字节的区块链数据。下一步与前一部分中所读到的几乎相同,但我们会再次介绍一遍以避免潜在的混乱。

  2. 你现在需要在本地创建一个以太坊账户,稍后会用来为你的区块链使用。要执行此操作,请运行以下命令:

$ geth account new
  1. 你需要两次输入密码,所以一定要记住它。之后运行puppeth命令:
$ puppeth

这会导致以下输出:

如果你无法执行puppeth或者在尝试时出现错误,那是因为你安装geth时没有安装它。请确保重新打开 Geth 安装程序,并在询问时勾选开发工具,因为那里才包括了puppeth。然后重新打开你的命令行或终端窗口查看变化。

  1. 你会看到它要求你管理一个网络。创建一个随机名称,稍后用来识别你自己的自定义区块链。在我的例子中,我使用merunas作为网络名称。

  2. 接着,它会要求你提供额外的指令。选择第二个选项 – 配置新的创世块 – 因为我们需要创建一个创世文件,这是它的第一个区块:

  1. 然后选择第一个选项:从零开始创建新的创世。它会要求你选择要使用的共识引擎。在这种情况下,我决定使用Ethash,因为它是真实 Mainnet 使用的那一种,尽管Clique也是一个用于实验权威证明的好选择:

  1. 接下来,你需要提供你刚刚创建的 Geth 账户,因为它需要一个预充值账户用于初始化。只需复制并粘贴你收到的地址。如果你忘记了,打开另一个终端,输入geth account new来生成一个新账户后继续。

  2. 然后,它会问你是否应该为预编译地址预充 1 wei。说no因为你不需要这些账户,它们会在你的创世块文件中填充不必要的配置。最后,为你刚刚设置的区块链指定一个网络 ID。我建议使用一个大于 10 的数字,因为较低的数字已被主网络和测试网络使用,所以你需要使用一个唯一的标识符以避免重放攻击:

  1. 恭喜!你刚刚配置了你的创世区块文件,这是你自己私有以太坊区块链的基石。现在,你需要导出它。通过选择第二个选项- 管理现有创世块 - 然后再选择第二个选项:导出创世块配置

  1. 现在,你应该把配置文件放在选定的文件夹中。要在 Geth 上加载你的自定义区块链,使用这个命令和你刚刚导出的自定义初始区块文件位置:
$ geth init <your-genesis-file-location>.json 

例如,在我的情况下,如下所示:

$ geth init 'C:\Users\merun\Desktop\puppeth blockchains\merunas.json'

我使用了引号,这样我就可以在文件夹名称之间包含空格,因为我创建的文件夹叫做puppeth blockchains,当我在没有引号的情况下运行命令时,会出错:

  1. 如果出现错误,提示Failed to write genesis block: database already contains an incompatible genesis block,只需执行以下操作:
$ geth removedb

当你运行geth removedb时,你正在删除已下载的区块链,解决了不兼容创世区块的问题。这主要是因为你在某个时候开始下载区块链时使用了一个创世文件,而你决定在某个时候使用不同的创世文件导致的。这是无效的,因为配置永久存储在第一个区块中,也就是创世区块中。在挖掘创世区块后,你无法更改区块链设置而不先删除之前的区块链。

  1. 最后,执行以下命令来运行你的区块链:
$ geth --networkid 100 --unlock 0x<YOUR-ETHEREUM-ADDRESS> --mine 
  1. 它会要求你输入你账户的密码。输入密码后,你会看到区块链准备好挖矿了。现在,你可以将它用于任何你想要的项目,包括像之前看到的那样连接到 Remix IDE。你可以使用完全相同的命令来做到这一点:
$ geth --nodiscover --networkid 77 --datadir .ethereum/ --unlock 0xf30c37b1e5ed82eebd1a7cf4c66cb9497faa4799 --mine --rpc --rpcapi eth,net,web3 --rpccorsdomain https://remix.ethereum.org

现在,你可以在 Remix IDE 上连接节点,这样你就可以在你自己的区块链上部署你的合约了。

选择一个以太坊钱包

以太坊钱包有多种选择,从软件钱包到硬件甚至纸质钱包,如果你不了解它们之间的区别,那么选择最适合你需求的最好的钱包会变得越来越困难。因此,在这一部分中,我们将探讨钱包的各种选择,以便您为每个项目选择最佳的钱包。

理解以太坊钱包的概念

看起来很直观——一个钱包是你存储加密货币的地方——但实际上,钱包比它们看起来复杂得多。钱包连接到以太坊区块链,以查找您最新的余额,通过智能合约、dApps 或者通过区块链客户端本身访问。以太坊客户端下载整个链数据,它们能够搜索与您的账户相关的交易。其中一些将是转账,而其他可能是智能合约执行,您需要支付燃气。最后,只需读取最新区块中包含的与您地址相关的交易,就可以获得您当前的余额,如下图所示,余额在几次交易后被更新:

由于安全性在处理资金时是一个很大的问题,钱包必须保护您的凭证可能被使用的每一个脆弱区域。您的私钥、种子和地址不应该被任何人轻易访问。这就是为什么有不同的钱包实现。它们可以是在线的或离线的,您可以进一步使用专门用于存储加密货币的离线硬件钱包。

解释在线钱包

这些钱包只有在您有互联网连接时才能访问,因为它们是基于网络的,允许您在几乎任何地方与您的钱包进行交互,只要您是连接的。请注意,它们并不总是在他们的服务器上保存你的私钥:有些是,但大多数要求你每次想要访问你的钱包时都提供你的私钥。让我们看一些例子来了解它们是如何工作的。

选择 MyEtherWallet

MyEtherWallet 是最流行的以太坊钱包应用程序,您可以用它来访问您的钱包。每当您想要登录时,它会要求您提供您的私钥;因为它不会在线存储您的凭证,它只是一个与您的资金进行交互的平台:

当你打开myetherwallet.com时,你会看到一个清晰的框,指示你可以创建一个新的钱包。如果你已经有一个,只需点击发送以太币和代币选项卡,它将要求你以不同的格式选项上传你的私钥。

如果您还没有钱包,点击 创建新钱包 来生成一个新帐户并输入密码。然后,它会要求您保存您的私钥并将其存储在安全的地方。请记住,您的私钥是您的钱包的主要元素:如果您丢失它,您将无法访问它,您的资金将永远丢失。

使用 MetaMask

MetaMask 是另一个著名的钱包,主要用作中介钱包,人们将以太币发送到其中以与去中心化应用和智能合约进行交互。MetaMask 在后端使用 INFURA,因此与区块链的连接是即时的,您不必下载数百个区块。

这个钱包的有趣之处在于它会将 web3.js 注入到您正在检查的任何页面中,这意味着只要您已经登录了 MetaMask,您就可以使用任何 dApp:

每当您想通过在去中心化应用上进行交易与区块链进行交互时,MetaMask 将会打开一个弹出窗口,询问您需要花费多少钱,包括燃气价格。然后,您就可以直接从浏览器中进行交易。

关于这个钱包的另一个好处是您可以随时通过几次点击更改您的区块链网络。这对开发者来说非常棒,因为您可以将您的智能合约部署到 Ropsten,然后再到 Mainnet,如果一切看起来都正确。总的来说,这是一个非常适合开发者和用户的出色钱包,他们都想与去中心化应用进行交互,包括 ICO。

理解硬件钱包

这些是将您的资金离线保存在一个小设备中的钱包,该设备安全地存储您的加密货币。关于这些设备的有趣之处在于它们不使用您的私钥进行交互;它们有一个系统可以在不将您的私钥置于风险的情况下将您登录到钱包中,因为它们将私钥存储在内部并进行了加密以增加安全性。

最受欢迎的是 Ledger 和 Trezor。这些公司制造了不同的设备,您可以使用种子设置,然后与您的加密货币一起使用,只要它们工作。请注意,它们与打印的私钥一样好,这意味着没有人会在线上访问它们,如果您不丢失它们,它们将保护您的资金安全。把它们看作是钥匙。如果它们损坏或者丢失了,您将永远丢失您的资金。这就是为什么我总是建议您购买几个相同的设备副本,三个或更多个具有相同的私钥和种子,这样即使其中一个损坏,您也可以访问相同的钱包。

使用 Ledger

Ledger 和流行的 Ledger Nano 看起来像传统的 U 盘,上面有一个 OLED 屏幕和两个按钮:

您可以通过 USB 线连接它到您的计算机,并打开一个定制的钱包应用程序来使用它。您可以使用 MetaMask 和 MyEtherWallet 等应用程序与此硬件设备一起使用。它通过使用一个 4 位数字 PIN 和一个密码(如果您在创建钱包时设置了密码)来安全地保存您的资金,就像信用卡一样。您可以存储成千上万种不同的加密货币,包括以太币,这些加密货币将保存在同一设备中。

使用 Trezor

Trezor 是另一款流行的硬件钱包,具有独特的扁平设计,同样配有两个按钮和一个高品质的黑白显示屏。此设备包装密封,必须完全破损才能访问钱包,以确保在您第一次拿到它之前没有人触摸您的 Trezor,因为一个黑客使用后可能会造成巨大的损害。

您还会收到几张小块的硬纸板,用于写下您的独特种子,以便在您无法访问钱包时恢复您的账户。确保不要丢失这些纸钱包,因为它们在 Trezor 固件更新时将会被要求:

两个按钮用于在您检查到它们与您计算机上显示的内容相符后确认交易。这是一个额外的安全层,以防止人们将资金发送到错误的地址。它还支持数百种加密货币,并具有与 Ledger Nano 相同的安全功能。最终,这取决于个人偏好,因为大多数这些硬件钱包的功能方式相同,并且足够安全。

学习使用多重签名钱包

多重签名钱包是一种特殊类型的钱包,因为它们实际上是实现了多重签名功能的智能合约,使得一组成员在他们都同意结果时可以签署交易。与这些钱包交互的唯一方式是直接使用智能合约或通过 Parity 应用程序,因为 Parity 创建了所有应用中最大的多重签名钱包。

要与此类钱包交互,只需打开 Parity 的 Web 界面,并从账户部分使用参与其中的成员的地址部署一个新的多重签名钱包。这些钱包的工作方式是,每个成员都可以提出一个提案,将特定金额的以太币从钱包转移到给定地址。如果他们都同意,资金将被转移;否则,转账将被取消。这是一种简单的方式,让加密货币初创企业将资金保留在一个团队基金中。

您刚刚获得了所有可用以太坊钱包类型的高级概述。现在,您可以选择符合您要求的钱包,并且可以开始安全地存储 ETH,以确保您需要。请记住,只要能让您访问自己的资金的钱包就是好的,所以请确保选择一个没有限制您访问权限并且多年来已经证明安全的钱包。

使用 Etherscan 进行交易

Etherscan 和所有其他区块浏览器一样,在您需要深入了解智能合约如何在更深层次上运行时,提供大量有价值的信息。了解如何使用 Etherscan 等区块浏览器是一项宝贵的技能,任何以太坊开发人员都需要能够调试和理解区块链交易的真实世界事件。

理解区块浏览器

区块浏览器是一种特定类型的应用程序,通过搜索可能与用户相关的特定信息,与区块链进行交互。它们是用于理解代码背后发生的事情的区块链机器人。它们被创建是因为人们想要一种更简单的方法来理解交易的发生,因为大多数交易都是混乱的,当您得到的只是十六进制信息时,很难发现重要的信息。

最受欢迎的是 Etherscan、Etherchain 和 Ethplorer。所有这些区块浏览器都是基于 Web 的,因此您可以轻松从任何连接到互联网的设备上使用它们。

使用 Etherscan 分析交易

Etherscan是迄今为止最大的区块浏览器,因为它提供了简洁清晰的界面,用于与您的交易进行交互。Etherscan 主要是一个地方,可以查看您部署后的交易实际在做什么。例如,当您将 10 ETH 发送到另一个地址时,您不知道是否成功,或者使用的地址是否有错误。Etherscan 将准确地显示发生了什么以及原因:

假设有人因为能够访问您的以太坊账户而偷了您 1 个 ETH。您如何跟踪这样的信息?那么,您可以将您的以太坊地址粘贴到 Etherscan 的搜索框中,然后查看它去了哪里:

只需单击 TxHash 即可查看有关特定交易的详细信息,您将看到以下内容:

这告诉您以下内容:

  • 交易成功了

  • 这是一个简单的将 1 个以太币转移到地址 0x8c5a579a06fac3b723d97061e295582de2825035 的转账,来自你自己的地址

  • 它是在 2019 年 1 月 30 日制作的

有了这些信息,你可以开始研究谁在那个地址背后,如果情况是这样的话,你可能找到谁偷了你的资金。在输入数据部分,你会看到被执行的代码,如果这是一个合同交易的话。你还可以使用 Etherscan 来检查代币、智能合约、图表等等,因为他们致力于为你提供有关区块链交易的最佳信息。

你还可以使用 web3.js 和 Truffle 合同创建自己的区块链浏览器!

现在你知道区块浏览器对以太坊开发人员来说是很棒的工具,因为它们以简单易懂的格式为你提供了宝贵的信息,这在处理数十万美元的永久主网交易时至关重要。

创建账户

账户是每个以太坊应用程序的主要组成部分。如果你想参与网络,你需要一个带有私钥和地址的以太坊账户。你可以通过许多方式创建以太坊账户,所以最好熟悉不同的工具,这样你就可以熟练地进行账户创建的过程:你将需要为你构建的所有项目创建账户。

使用 MetaMask 创建账户

正如你已经知道的,MetaMask 是最流行的以太坊钱包之一。它允许你为不同的以太坊链创建无限的账户,没有限制,使用相同的种子单词。以下是使用它创建账户的方法:

  1. 首先,通过访问metamask.io在 Chrome 或 Firefox 上下载 MetaMask:

  1. 然后,你可以点击刚刚安装的狐狸图标来启动 MetaMask,它将打开这个页面:

  1. 点击“继续”并为你的钱包设置密码,然后向下滚动以接受使用条款:

  1. 之后,它会向你显示不同的窗口,警告你使用像这样的测试软件的风险,并告诉你除非你完全确信这个过程,否则绝对不要将恢复种子提供给它们。然后,你需要点击并将你的种子短语存储在一个安全的地方。这是一组独特的单词,将帮助你恢复账户,如果你丢失了它。这是你的主要私钥,绝对不能丢失,否则你的钱包将无法访问:

  1. 最后,它会提示你使用 Coinbase 或 ShapeShift,或者直接存入以太币:

  1. 你可以简单地忽略这一点,并稍后添加资金。就是这样——现在你应该能够通过点击扩展栏上的小橙色狐狸来使用 MetaMask:

使用 Coinbase 创建账户

现在您已经了解了如何创建自己的私人钱包,让我们创建一个托管在交易所中的钱包,例如 Coinbase,在这里您可以直接购买和出售加密货币。请注意,这些类型的钱包不安全,因为交易所保留您的私钥和种子短语,因此您无法完全控制您的钱包,也无法自行保护它。

这种类型的钱包的一个优点是,您几乎可以立即将您的加密货币兑换为货币,而无需等待转账。这对于需要快速访问其资金的交易者非常理想。这样做的主要问题是缺乏控制,因为您提供给他们您的密钥,并且您依赖于他们保持其安全——这并不总是事实。始终查看交易所的历史记录,以查看它们是否值得信任,或者您的资金是否有可能被可能的黑客攻击所威胁。

Coinbase 是最大的加密货币交易所,因其提供的简便性而受到欢迎,您可以使用银行或信用卡将法定货币兑换为加密货币。在以下步骤中,您将了解如何创建一个与 Coinbase 关联的以太坊账户:

  1. 首先,打开他们的网站 coinbase.com

  1. 在那个大输入框中键入您的电子邮件地址,然后单击“开始”以创建新的 Coinbase 账户。然后,它会要求您提供更多所需的详细信息,以便您可以开始:

  1. 随后,它会向您显示一些有关法律问题和警告的信息:

  1. 然后,您将不得不检查您的电子邮件以确认您的地址:

  1. 确认您的电子邮件地址和登录后,您将被要求添加您的电话号码以进行安全措施,因为此类账户处理大量资金,他们希望拥有最大数量的安全点:

  1. 他们将向您发送一条消息,请务必使用有效的号码。之后,您将被要求提供个人信息,例如您的居住地、职业和身份证号码。只需填写所有框:

  1. 提供有效数据非常重要,因为他们会检查所有内容,并使用该信息帮助您安全交易。之后,您将被要求通过护照或驾驶执照的照片验证您的身份;您必须这样做才能在此进行交易。这很烦人,但法律要求,因为他们正在与银行打交道,他们不想留下漏洞:

  1. 如果这不起作用,请稍后再试,直到您获得访问权限。最后,在完成所有文件并再次登录后,您将能够在“账户”选项卡中交易并查看您的账户:

经过这些步骤,您应该能够访问一个功能齐全的基于 web 的钱包,该钱包与 Coinbase 相关联,接受许多不同的加密货币和法定货币。一旦添加了您的银行账户详细信息,您应该能够购买加密货币。

摘要

在本章中,您学习了如何为不同钱包创建以太坊账户,其中一些是私人的,而其他一些是托管在交易所中,以便简化获取新加密货币的流程。您了解了它们之间的区别以及它们的用途。您现在知道 MetaMask 很适合与 dApps 交互并安全存储您的资金,而 Coinbase 很适合使用法定货币立即进行交易和交换加密货币,等等。

在下一章中,您将更多了解 dApps 及如何使用高级技巧全速运行它们,这些技巧您可以立即实施,以创建更强大的应用程序,以便在未来进行扩展。

第二部分:去中心化应用程序开发工作流程

你将了解到许多受到区块链影响的行业如何通过以太坊得到改进,以便你可以实际了解以太坊在市场上的整体状况。每个行业都将有项目来巩固你学到的知识。

本节将涵盖以下章节:

  • 第四章,精通智能合约

  • 第五章,精通 dApps

  • 第六章,工具、框架、组件和服务

  • 第七章,在测试网上部署

  • 第八章,各种 dApps 集成

  • 第九章,去中心化交易所工作流程

第四章:精通智能合约

在本章中,您将被带上一段旅程,穿越几个有趣的框架,以设计安全、可扩展和快速的智能合约,这些智能合约被优化以减少 Gas,从而使人们不必为每笔交易支付太多以太币。我们将创建一个版权市场智能合约,该合约存储人们为其内容创建的许可证,这些内容由唯一的 ID 标识,并且全部存储在区块链上。然后,您将深入探讨 EVM,以了解在部署和与智能合约交互时,它在后台实际运行的方式。接下来,我们将讨论下一代可升级和可扩展的智能合约。最后,我们将涵盖 Gas 和数据高效的智能合约以及安全分析。在本章中,我们将涵盖以下主题:

  • 适应速度

  • 深入了解 EVM

  • 可升级和可扩展的智能合约

  • Gas 和数据高效的智能合约

  • 智能合约安全分析

适应速度

如果你想要成为智能合约方面的专家,你必须从一个简单的项目开始,以更新你的技能。我们将创建一个版权市场智能合约,将合法文件上传到区块链,以确保某些内容受版权法保护,不能被复制。言归正传,让我们开始吧!

规划市场构想

这里是版权法的简单工作方式:

  1. 你要注册一篇内容,可能是文件、文本、图片、视频或者你自己制作的任何一种创作,通过一个唯一的标识符公开注册,这样其他人可以确认你的权威性。

  2. 无论出于什么原因,想要使用你的内容的人都必须咨询您的版权条款并遵守它们,以避免法律问题,因为我们不希望人们为了自己的利益而窃取你的作品。

版权注册不应该被修改,所以我们会避免创建允许人们修改其版权数据的功能;因此,我们只允许他们添加或删除版权。

我们需要一个生存在区块链上的智能合约,人们可以轻松访问。该合约将具有以下功能来管理版权注册:

  • 一个根据哈希值创建新的带有唯一标识符的版权内容的函数。在这种情况下,我们只想限制版权内容为文本,因为在区块链上上传其他类型的内容是昂贵的。该函数将接收内容所有者的地址、姓名、电子邮件地址、内容的哈希 ID、包含要注册的文章或文档的 URL,以及他人在使用该特定内容时必须遵守的使用条款。

  • 一个根据哈希值获取内容的函数。

  • 一个用于删除版权内容的函数,如果你是所有者。

  • 一个用于提取锁定在此智能合约中的资金的函数。经常发生的情况是,人们将以太发送到错误的地址,最终进入了这样的智能合约。如果出现这种情况,我们希望能够提取被锁定的资金,以免永远丢失。如果这种情况发生,它也可以用作显示对完成工作的感激的捐赠地址。

Solidity 代码设计

在这个项目中,我们将使用 Truffle 来部署和运行我们的智能合约。当处理更大型的项目时,始终值得在设置诸如 Truffle 之类的框架和所有必需的依赖项时投入初始工作,因为这样可以节省我们大量的时间,同时提高开发体验。以下是使用 Truffle 设置项目的步骤,因为这是我们在本书中首次提到它。稍后,我们将假设您知道如何在 Truffle 中设置项目,尽管在需要时我们会提醒您很多次:

  1. 让我们立即开始创建一个名为copyright的新文件夹

  1. 在该文件夹内,运行truffle init命令。如果您还没有 Truffle,请使用npm i -g trufflesudo npm i -g truffle安装它。您可以通过从他们的网站nodejs.org安装 Node.js 来获取npm。下面的截图显示了这个过程:

  1. 然后,您可以使用您喜欢的代码编辑器打开项目,我使用的是 Atom:

  1. contracts目录下创建一个名为Copyright.sol的新文件。创建每个 Solidity 智能合约的基本代码结构:
pragma solidity 0.5.0;
contract Copyright {}

每个智能合约的顺序很简单。首先,我们定义事件、变量、修饰符、回退函数、构造函数,最后是函数。所以,让我们定义我们的事件:

pragma solidity 0.5.0;
contract Copyright {
    // Create the events
    event RegisteredContent(uint256 counter, bytes32 indexed hashId, string indexed contentUrl, address indexed owner, uint256 timestamp, string email, string termsOfUse);
}

此事件记录了counterhashIdcontentUrl、所有者的地址、时间戳、电子邮件和该内容的使用条款。现在我们可以定义包含这些变量的struct。我们的目标是以尽可能易于访问的方式存储信息,因此使用结构体是以映射方式组织受版权内容的最佳方式:

pragma solidity 0.5.0;
contract Copyright {
    // Create the events
    event RegisteredContent(uint256 counter, bytes32 indexed hashId, string indexed contentUrl, address indexed owner, uint256 timestamp, string email, string termsOfUse);

    // Create the variables that we'll use
    struct Content {
        uint256 counter;
        bytes32 hashId; // The half keccak256 hash since we can't store the entire 64 bit hash
        string contentUrl;
        address owner;
        uint256 timestamp;
        string email; // We need a valid email to contact the owner of the content
        string termsOfUse;
    }
    mapping(bytes32 => Content) public copyrightsById;
    uint256 public counter = 0;
    address payable public owner; }

映射将把每个哈希与存储了有关受版权内容的所有相关信息的内容对象相关联。以下是结构体中每个成员的原因:

  • counter:我们需要一种简单的方法来跟踪添加到区块链上的受版权元素的数量,因为它为我们提供了一种使用事件注册表查找最新内容的简单方法。

  • hashId:我们将与此对象关联的文本内容的keccak256字符串。因为我们不能将字符串用作映射的键,所以我们必须使用bytes32类型的变量。这导致了我们无法存储整个keccack256字符串的问题,因为它有 64 个字符长,而bytes32变量限制为 32 个十六进制字符。为了克服这个限制,我们将做的是将结果哈希值截断一半,仅存储前半部分,即正好 32 个字节。这显着降低了哈希安全性,但考虑到我们不会生成天文数字般的高版权元素,这已经足够了。

  • contentUrl:包含原始文档的 Web URL,用于版权登记。

  • owner:内容所有者的地址,这对确保个人财产至关重要。

  • timestamp:内容版权的固定时间戳。非常适合设置到期日期以使过时元素失效。

  • email:有效的电子邮件,提供给用户一个简单的联系方式,以联系特定内容的所有者。

  • termsOfUse:简短的文本,解释了如何处理内容:外部个人可以做什么,何时可以使用等等。

如果您希望创建不同类型的版权注册市场,可以向struct中添加自己的元素。这取决于个人偏好。最后,我添加了一个owner变量,我们稍后将用于提取可能被锁定在合同中的资金。现在,我们可以继续进行功能:

// To setup the owner of the contract
constructor() public {
   owner = msg.sender;
}

// To add new content to copyright the blockchain
function addContent(bytes32 _hashId, string memory _contentUrl, string memory _email, string memory _termsOfUse) public {
   // Check that the most important values are not empty
   require(_hashId != 0 && bytes(_contentUrl).length != 0 && bytes(_contentUrl).length != 0 && bytes(_email).length != 0);

   counter += 1;
   Content memory newContent = Content(counter, _hashId, _contentUrl, msg.sender, now, _email, _termsOfUse);
   copyrightsById[_hashId] = newContent;
   emit RegisteredContent(counter, _hashId, _contentUrl, msg.sender, now, _email, _termsOfUse);
}

constructor变量将用于设置所有者地址。addContent函数需要四个参数,而不是结构所需的七个,因为我们可以动态生成其他参数。这对于简化事务并帮助人们更轻松地上传其版权非常重要。首先,它检查是否设置了最重要的参数,因为无论如何我们都需要它们。这意味着使用条款是可选的,简单地因为人们可以选择不限制其内容的使用,这意味着使用其所有权的开放内容。

我们生成newContent对象,增加计数器,并将该内容添加到copyrightsById映射中,最后发出注册事件。这是最重要的功能,必须尽快进行优化,以避免达到气体限制。

注意,我们不需要一个根据哈希获取内容的函数,因为copyrightsById映射是public的,因此它会自动为检索每个结构元素创建一个 getter 函数。

让我们继续完成删除版权和提取资金的功能:

// To delete something if you're the owner
function deleteCopyrightedByHash(bytes32 _hashId) public {
    if(copyrightsById[_hashId].owner == msg.sender) {
        delete copyrightsById[_hashId];
    }
}

// To extract the funds locked in this smart contract
function extractFunds() public {
    owner.transfer(address(this).balance);
}

deleteCopyrightedByHash函数获取现有版权的一半keccak256哈希,并在您是特定内容的所有者时将其删除。extractFunds函数是一个可选函数,我喜欢添加这个函数,因为我通常看到智能合约中包含了以太币,这是由于某人不小心将真钱发送到合约地址,所以它一直停留在那里。此函数使提取资金成为可能,以便如果发生这种情况,您可以检索资金。

这就是整个合同!您可以使用truffle compile来编译它。

您可以在我的 GitHub 上查看更新后的代码:github.com/merlox/copyright-marketplace.

使用 Truffle 部署您的智能合约

使用 Truffle 部署智能合约并不是一个直截了当的过程:您必须通过几个不同的文件来修改 Truffle 的预期行为。您会发现,Truffle 需要知道在哪里部署您的合约、构造函数参数和部署顺序,以及其他事项;因此,有必要配置所有运动部分以连续部署您的合约。设置完成后,只需一条命令,您就可以重新部署新版本,因为 Truffle 将确切知道您需要完成什么任务,如何完成,以及在哪里完成。

我们将按照以下步骤部署我们的合约:

  1. 首先,您必须使用truffle compile编译您的合约,因为您只能将编译代码部署到区块链上。打开项目文件夹中的truffle-config.js文件。这是设置部署配置的文件。

  2. 如果您阅读评论,您会发现 Truffle 使用 INFURA 进行快速有效地连接到区块链,您需要取消注释 1 到 5 行以更新您的信息。例如,此处显示的注释代码必须取消注释:

这样做将意味着看起来像下面这样:

这实质上是取消注释了您的 INFURA 和助记词变量。

  1. 注册infura.io获取您的 INFURA 密钥,以便在创建项目后,可以无限制访问安装了区块链的高质量服务器:

  1. ENDPOINT 是您的 INFURA 密钥所在的地方。只需单击旁边的复制图标,这样您就会得到类似以下内容:
https://ropsten.infura.io/v3/8e12dd4433454738a522d9ea7ffcf2cc
  1. 取消复制字符串,只保留网站名称后的部分,因为这是 Truffle 在配置部署网络密钥时使用的内容:
v3/8e12dd4433454738a522d9ea7ffcf2cc
  1. 现在,将其粘贴到truffle-config.js中的infuraKey变量中,以便您能够在ropsten上部署您的智能合约:

  1. 现在您需要给 Truffle 提供您的助记词短语,以便它能够使用您的地址在ropsten上部署智能合约。为此,请在项目的最顶层创建一个名为.secret的文件,其中包含助记词。以下是一个示例:

  1. .secret中,仅写下您的助记词,不要添加任何其他信息:

  1. 然后,返回到您的truffle-config.js文件,并取消对ropsten部分的注释:

  1. 然后,使用以下命令安装truffle-hdwallet-provider
$ npm i truffle-hd-wallet-provider
  1. 在安装它时,请确保您在项目文件夹中,因为它将在当前位置安装。最后,执行以下操作:
$ truffle deploy --network ropsten

您的合约将在几分钟内部署完成,具体时间取决于网络情况。然后,您可以玩耍并测试智能合约。确保使用此工具对受版权保护的内容进行keccak256哈希处理:emn178.github.io/online-tools/keccak_256.html

获得keccak256后,将其切成一半并生成十六进制代码。如果您有 MetaMask,则可以在浏览器中执行此操作,因为您将在所有网站上都可以使用 web3.js。以下是将特定文本字符串转换为其十六进制版本的步骤:

  1. 在您的浏览器上打开开发者工具。

  2. 转到您可以执行 JavaScript 代码的 Console 标签。

  3. 输入web3以查看 web3 是否可用。

  4. 如果是这样,请取一半的keccak256结果代码,并使用web3.toHex('your-hash')生成十六进制版本,如下例所示:

web3.toHex('041a34ca22b57f8355a7995e261fded7')
"0x3034316133346361323262353766383335356137393935653236316664656437"

然后,您可以使用该十六进制字符串将新的版权元素添加到智能合约中,作为hashId

深入了解 EVM

以太坊虚拟机(EVM)是整个以太坊生态系统中最不被理解的部分之一。这可能是因为即使对于最优秀的开发者来说,这个虚拟机包含的复杂性也令人困惑。无论如何,作为以太坊专家,您的目标是成为与以太坊开发相关的所有事物的专家,因此您必须理解这台强大的虚拟机的复杂性,以便随时发挥其全部潜力。

解释 EVM

EVM 是一个虚拟机,用于执行智能合约和交易。将以太坊操作系统与 VirtualBox 或 VMware 类比。它们是物理计算机的仿真,以创建一个应用程序可以与 CPU 通信的清洁环境。这种虚拟机为所有用户统一处理交易、区块和智能合约。它保持数百台计算机连接,因此有足够的安全性来保护每个节点免受潜在攻击是非常重要的。一些已实施的安全系统如下:

  • Gas:每笔交易在执行之前必须付费,以避免某些节点发送无限交易而没有意图支付它们的滥用行为。

  • 点对点通信:节点只能通过发送和接收数据的消息进行通信,这意味着它们无法访问彼此的数据。

  • 确定性:这意味着初始状态总是会产生相同的结果。例如,如果两个数字相加结果相同,那么无论它们是什么,都会得到完全相同的结果。这对于实现共识是很重要的,因为它允许他人验证交易确实有效;否则,要验证交易并取得共识将是不可能的,因为相同的状态导致不同的计算。

  • 沙盒执行:由于这是一个虚拟机,使用它的智能合同不能访问外部计算机,使所有交易都成为可能。只有智能合同才能以有限的范围相互交互。

要完全理解 EVM 的工作原理,我们必须深入研究保持系统一体的汇编语言。理想情况下,我们可以理解字节码语言,这是计算机处理的内容,但由于我们无法理解,我们已经为每个过程创建了一种称为汇编语言的语言,它可以轻松转换为处理器计算的字节码。

EVM 中智能合同的操作

智能合同非常吸引人,因为它们能够在一个能够理解它们应该如何做出决策并就结果达成一致的单一系统中保持许多节点连接。它们在一个基于堆栈的虚拟机上运行,您可以将其视为一个数组,用于跟踪存储在内存中的变量。它为您提供了一组小函数来操作该堆栈。要了解智能合同在 EVM 中的操作方式,我们必须跟踪它们,以了解它们如何被虚拟机转换为可执行的字节码。

例如,考虑一个简单的合同,比如这个:

pragma solidity ⁰.5.0;

contract Example {
    uint256 myNumber = 3;
}

编译器(比如solc)是如何将代码转换为 EVM 可以理解的东西,以便计算机可以处理它的呢?让我们尝试用特定的编译器进行分析。通过执行以下命令安装solc

$ npm i -g solc

如果您将合同编译为字节码并分析字节码所采取的过程,您将得到以下结果:

$ solcjs Example.sol --bin

您会看到有关 EVM 行为的一些有趣的事情。首先,您会得到一个带有.bin结尾的文件,例如Example_sol_Example.bin,这是包含智能合同已编译字节码的二进制文件:

60806040526003600055348015601457600080fd5b5060358060226000396000f3fe6080604052600080fdfea165627a7a72305820aa17e74115b5e066ae13d560c624e9abef54adbce68c3443886eadc4e1059cfe0029

要理解所有那个字节码并看看 EVM 真正在做什么,我们必须根据每个指令拆分它。6080604052只是智能合同的初始化。每个合同都是一样的。然后,我们有以下内容:

60 03

数字 60 是一个特定的指令,被称为PUSH,它本质上将数字3移到内存堆栈中。堆栈只是一个空数组,可以从数组中获取或删除值。在PUSH 3 指令之后,我们的堆栈看起来像这样:

stack: [3]

以下指令将零添加到堆栈中:

60 00

这导致stack: [0, 3]

然后,我们有55,这是一个STORE指令,它将uint256永久写入存储,即区块链。我们需要在将其写入存储之前将数字 3 添加到堆栈中。我们在这里做的实质上是以下内容:

uint256 myNumber = 3;

这相当于以下内容:

6003600055

请记住,所有智能合约都以6080604052开始,就像你在完整的字节码中看到的那样。我们的智能合约只包含一个uint256赋值,因此它遵循具有6003600055的字节码。我们最终得到的是以下内容:

60806040526003600055348015601457600080fd5b5060358060226000396000f3fe6080604052600080fdfea165627a7a72305820aa17e74115b5e066ae13d560c624e9abef54adbce68c3443886eadc4e1059cfe0029

其余部分是无关紧要的,因为它包含关于发送者和合约的信息。大约有 100 个操作码,在 EVM 中使用许多不同的技术将智能合约代码转换为字节码,以便可以有效地发布到区块链上。

现在,你了解了当你编写智能合约、编译它并将其部署到网络时,在幕后发生了什么。其余工作由运行其自己特定实现的以太坊协议的节点来完成,因此它们决定采取哪些块并忽略哪些块。EVM 是一个复杂的虚拟环境,它处理来自智能合约的事务和字节码,以实现符合所有人利益的全局共识。

可升级且可扩展的智能合约

以太坊区块链是不可变的,这意味着你无法修改其过去的操作。一旦一个动作被大多数矿工确认,该动作将永远留在那里。相同的限制也适用于智能合约。然而,我们作为以太坊专家,能够克服部署不可扩展代码的问题,因为我们理解存在着不同的开发技术,我们可以创建可扩展的代码。这个想法是部署智能合约,使它们准备好在未来扩展,具有一组不同的、可互换的合约。知道如何创建可扩展并且能够在未来改进的智能合约是任何顶尖以太坊专家必须具备的基本技能,所以不要错过它。

可升级智能合约

可升级智能合约是创建面向不断发展、突然需要新功能的大型行业的先进智能合约的下一步。它们可以用于各种项目,但我建议你只为那些可能需要未来升级或想要为未来预测保护数据的项目使用它们。并不是每个智能合约都需要具有可升级功能,尽管代币等项目将受益于可升级的代码。

我们将创建一个由三部分和两个较小智能合约组成的可升级智能合约。它将是这样的:

有许多方法可以使智能合约可升级 - 你甚至可以创建自己的!因此,在理解这种技术时要有耐心,因为在开始阶段可能会感到困惑。

要使合约可升级,我们需要三个部分:

  • 代理合约:这是初始合约 - 用户始终用来与代码交互的主要合约。把这个视为同样旧的智能合约,用于进行函数调用,但是,不是在这里执行逻辑,而是在可升级智能合约中执行。

  • 可升级合约:这是所有逻辑发生的地方。这个合约可以升级,这意味着它可以被移除,可以部署一个同名但带有新功能的新版本。用户不会直接与这个合约交互,因为地址每次升级时都会发生变化。

  • 存储合约:因为我们不希望每次升级合约时丢失我们的状态变量和所有用户信息,我们将所有变量、映射和数据存储在这个存储合约中。它只会有变量和设置器函数来更新这些变量。除此之外,它不会有任何逻辑或函数。

要升级一个合约并保持相同地址,将会发生以下情况:

  1. 用户将向代理合约发送一个交易,例如,执行函数 buyTokens()

  2. 代理合约将发送这个命令,找到可升级合约中的buyTokens()函数,并执行它。

  3. 可升级合约将处理购买 token 的逻辑,并将所有更改存储在存储合约中,例如,通过调用名为setTokens()的函数,将用户 A 的 token 数量增加到 40。

  4. 存储合约执行setTokens()并将 tokens 变量更新为 tokens = 40;,用于用户 A。

这基本上是逻辑操作的工作流程。实质上,我们将始终使用相同的合约地址,但逻辑会发生变化。我们也会保留相同的信息,而不会删除它,因为这是非常有价值的信息,很难重新插入。

为了帮助你理解所有这些合约背后的概念,我将向你展示每个智能合约具有的伪代码版本。我们将使用 token 比喻,其中用户想要在可升级合约中购买 token。

这是代理合约的样子:

contract Proxy {
    address public upgradableContractAddress;
    function () public {
        // Delegate the execution to the upgradable contract instead of using the code in this contract since this contract won't change, it's just to keep the address consistent and to have a registry of upgradable contracts
    }
}

这是可升级合约的样子:

contract Upgradable {
    address public storageContractAddress;
    function buyTokens() public {
        // This is the function that the proxy called in this example. So this contract will execute the needed logic to buy tokens and will update the state variables in the storage contract
        storageContractAddress.setTokens(userA, 40);
    }
}

最后,这是存储合约的样子:

contract Storage {
    mapping(address => uint256) public tokens;

    function setTokens(address _user, uint256 _number) public {
        // This function is used to update the storage variables since we don't want any logic to take place in this contract
        tokens[_user] = _number;
    }
}

这三个合同一起工作是可升级智能合同背后的核心概念,而不是更新整个代码库,我们将不同的角色分成可以在将来修改的可变部分。让我们深入了解它们是如何一个接一个地实现的。

存储智能合同

存储智能合同是这三个智能合同中最简单的,因为它只包含变量和函数来更新这些变量。我们不需要 getter,因为这些都是 public 变量,已经默认创建了 getter 函数来获取 public 变量。

我们将创建一个简单的存储智能合同,其中包含一个uint变量,以演示目的。然后,你可以根据需要添加更多变量。该合同非常简单,因为它只包含一个变量:

pragma solidity ⁰.5.0;

contract Storage {
    uint256 public myNumber;
    function setMyNumber(uint256 _myNumber) public {
        myNumber = _myNumber;
    }
}

正如你所见,我们有一个名为myNumber的变量和一个名为setMyNumber的 setter 函数来更新该变量。

你可能希望引入访问逻辑,只允许特定用户更新某些变量。目前,只要你理解它的外观就足够了。

可升级智能合同

可升级智能合同是最有趣的。它将执行所有逻辑,并在需要更新一些变量数据时与存储合同交互。让我们看看代码,看它是如何工作的。稍后我们将看到如何对其进行升级:

pragma solidity ⁰.5.0;

contract Upgradable {
    address public storageContract;
    constructor (address _storageContract) public {
        storageContract = _storageContract;
    }

    // A sample function that you could implement for buying tokens for demonstration purposes
    function buyTokens() public {
        // Do your logic for buying tokens for instance, calculating how many he will get for the msg.value he sent and so on. To later update the storage information
        // Create the storage contract instance
        Storage s = Storage(storageContract);
        s.setMyNumber(10);
    }
}

contract Storage {
    uint256 public myNumber;
    function setMyNumber(uint256 _myNumber) public {
        myNumber = _myNumber;
    }
}

这个可升级合同与Storage智能合同在同一个文件中。这一点很重要,因为在部署时我们需要访问该合同。或者,你也可以使用import关键字。

正如你所见,我用一个名为storageContract的变量声明了Upgradable智能合同,它跟踪存储合同的地址,因为我们将在那里更新状态变量。在构造函数中,我们设置了存储合同的地址,因为该合同将一直保留相同的存储,直到更新为止。然后,我添加了一个名为buyTokens()的函数作为示例,向你展示它在现实世界中是什么样子的;目前它除了通过调用Storage合同中的setMyNumber()更新存储外不会执行任何操作。

如果你要创建一个真正的实现,你将添加所有你期望的逻辑。我不想向你展示一个真正完整的可升级合同版本,因为其中许多函数会让你无法理解可升级智能合同是如何工作的。

它显示了你执行所有逻辑,然后在文件末尾,通过调用存储合同更新存储变量。

要升级这个合同,请按照以下步骤操作:

  1. 部署这个合同的新版本,其中包含了更新后的函数。你可以添加新的函数,更新旧的函数,甚至删除一些。

  2. 在部署合同时,在你的构造函数中使用存储地址,因为你需要访问该合同来保持变量。

  3. 最后,在代理合约中,执行一个名为upgradeUpgradableContract()的函数,它将把所有的函数调用重定向到你的可升级智能合约的新版本。

请注意,您可以部署这个合约,同时保留相同的存储合约,这样您的数据就可以安全地保留,并且在下一个可升级合约版本中准备好像什么都没有发生一样使用。或者,您可以部署一个新的存储合约,然后部署一个指向该新存储的新可升级合约。这样,如果您不想保留旧数据,您就会有一个干净的存储。然而,您仍然可以访问旧数据,因为合约将永久地存在于区块链上,只要在创建新的可升级合约时指向旧存储地址即可。

代理智能合约

这是最重要的部分,因为这个合约将永远不会改变。我们不希望它改变,因为我们希望保留相同的以太坊地址,同时更新底层逻辑。这个合约将把所有的调用重定向到可升级合约,并且有几个变量用于注册当前可升级合约的地址和不想更新到新代码的过去合约列表。

请记住,升级到新版本始终是可选的。如果您的用户决定要继续使用旧的智能合约,他们可以直接将所有的交易发送到旧的可升级合约而不执行代理合约。您可以通过使用 selfdestruct() 函数销毁旧的可升级智能合约来阻止他们这样做,但我不建议这样做,因为这将使您的合约变得毫无价值,并且可能在未来可用。

这就是代理合约的样子:

pragma solidity 0.5.0;

contract Proxy {
    address public storageAddress;
    address public upgradableAddress;
    address public owner = msg.sender;

    address[] public listStorage; // To keep track of past storage contracts
    address[] public listUpgradable; // To keep track of past upgradable contracts
}

这是最复杂的合约,因为它需要做一些事情。首先,我们设置了 storageAddressupgradableAddressowner。这些是合约将用来理解在哪里重定向所有调用的变量。实际上,我们并不需要跟踪存储地址或所有者,但这是一个良好的做法,因为它使事情更容易理解,并提供更多的选择。请注意,delegatecall() 函数无法更新可升级合约的存储,所以我们设置了一个外部存储。

然后,我们有两个地址数组:listStoragelistUpgradable。这些数组将包含当前和旧版本的存储和可升级合约,因为我们希望在需要时能够访问旧的逻辑。我添加了一个 onlyOwner 修饰符,因为我们只希望允许所有者进行合约更新。

之后,我们有构造函数。它被用来立即部署一个新的存储和可升级智能合约。您可以分别部署它们,但从代理的构造函数中这样做更简单和干净:

modifier onlyOwner {
    require(msg.sender == owner);
    _;
}

constructor() public {
    storageAddress = address(new Storage());
    upgradableAddress = address(new Upgradable(storageAddress));
    listStorage.push(storageAddress);
    listUpgradable.push(upgradableAddress);
}

接下来,有一个标记为外部的回退函数。这是最重要的函数,因为它是将接收所有函数调用并将其重定向到可升级智能合约的正确函数的函数。它通过使用delegatecall()函数工作。这是一个接收bytes参数以指示使用参数值从其他合同调用哪个函数的低级函数:

function () external {
    bool isSuccessful;
    bytes memory message;
    (isSuccessful, message) = upgradableAddress.delegatecall(msg.data);

    require(isSuccessful);
}

最后,我们有必要在所有者决定升级存储和逻辑合同时,升级存储和逻辑合同所需的函数。它们通过在数组中跟踪先前的版本来工作,以便用户如果希望如此,则可以访问旧版本:

function upgradeStorage(address _newStorage) public onlyOwner {
    require(storageAddress != _newStorage);
    storageAddress = _newStorage;
    listStorage.push(_newStorage);
}

function upgradeUpgradable(address _newUpgradable) public onlyOwner {
    require(upgradableAddress != _newUpgradable);
    upgradableAddress = _newUpgradable;
    listUpgradable.push(_newUpgradable);
}

要了解与逻辑合同通信的机制如何工作,请查看此函数:

upgradableAddress.delegatecall(msg.data);

首先,这需要调用合同的地址,本例中为可升级合同地址,然后是包含要传输信息的数据。msg.data参数是一个特殊变量,其中包含要调用的带参数值的函数。在这里,您将编写要调用的函数名称。例如,假设您想执行buyTokens(uint256 _number)函数,其中_number参数为12

在普通合同中,您只需创建合同实例,并按名称调用函数,同时发送参数值:

MyContract(contractAddress).buyTokens(12);

但是当我们事先不知道函数名称时,我们无法这样做。请记住,我们正在处理可能在将来具有新函数的合同,并且我们还希望从同一代理合同中访问这些函数。因此,我们使用msg.data。该变量包含一个十六进制字节字符串,其中包含函数名称和参数。让我们看看如何正确格式化它,以便 Solidity 理解我们要调用哪个函数。

首先,我们需要函数名称,然后我们需要编码的参数。因为 Solidity 使用十六进制值,所以我们需要用函数签名以十六进制形式写入它。函数签名是通过短的十六进制值而不是它们的字符串名称来引用函数的一种简短方式。如果您转到remix.ethereum.org,您可以快速看到每个函数的函数签名:

如您所见,我已经在 Remix 上复制了我的合同。现在,要以十六进制形式获取setMyNumberStorage()函数的函数签名,您必须在选择您的合同时转到编译选项卡:

然后,您可以点击“详情”以获取更多信息:

如果您向下滚动,您将看到合同的功能哈希部分:

setMyNumberStorage()的函数哈希是009be4e6;这是函数的签名。非常好!我们有了msg.data对象的第一部分:0x009be4e6

现在,我们需要编码函数的参数。假设我在调用函数时要设置一个数字为16。您可以通过使用 web3 手动将该数字转换为其十六进制形式。如果您已安装 MetaMask,您可以打开您的浏览器的开发者工具与web3.js交互。在控制台中,您只需输入web3.toHex(16),就会收到 16 的十六进制版本:

请注意,web3.toHex()仅适用于 web3 的 0.20 版本,这是目前由 MetaMask 使用的版本。在 web3 1.0 中,语法是不同的,因此如果 MetaMask 更新了它的 web3 版本,您可以在官方 web3 1.0 文档中阅读如何进行十六进制转换。

现在,我们只需要在 10 的前面添加 62 个零,因为0x10是我们要获得该参数的编码版本。这是一个例子:

0000000000000000000000000000000000000000000000000000000000000010

这将被十六进制编码。我们只需要在前面添加函数签名,以完成我们的msg.data对象:

0x009be4e60000000000000000000000000000000000000000000000000000000000000010

就是这样! 那一长串十六进制数字代表着:调用函数 setMyNumberStorage(12)

回到我们离开的地方,.delegatecall(msg.data)函数现在可以正确执行我们的函数调用的转换值。你可能在问自己:为什么要这么复杂,用户怎么会做所有这些转换工作呢?

人们使用您的智能合约或 dApp 的真相是,不需要他们做所有的转换。你只需在你的去中心化应用程序中简单地编写转换逻辑,函数名称将在几行代码中自动转换为 web3 中,就像你已经看到的那样。或者,您可以在 Remix 上部署您的合约,并在只需点击一个按钮即可获得整个编码函数调用:

当您部署合约时,您会看到一个框,您可以与部署的合约进行交互。如果你点击我标记的箭头,你会看到函数调用的扩展版本,显示给您一个工具箱图标,您可以点击它直接将setMyNumberStorage(16)转换为十六进制形式:

0x009be4e60000000000000000000000000000000000000000000000000000000000000010

这是在使用 Remix 与您的合约时进行转换的一个简单技巧。有了这个十六进制数字,您可以打开 MetaMask 并将其粘贴到数据字段中。首先,打开 MetaMask 并点击发送:

然后,通过向下滚动将您的代码粘贴到数据字段中:

确认交易后,您将看到您的函数已成功委托,并且您已经从代理合约执行了这个操作。

让我们回到我们的回退函数:

    function () external {
        bool isSuccessful;
        bytes memory message;
        (isSuccessful, message) = upgradableAddress.delegatecall(msg.data);

        require(isSuccessful);
    }

您可以看到delegatecall函数返回两个值:一个布尔值,用于指示委托是否成功,以及一个字节参数。

(isSucessful, message)

我们只是捕获它们并要求委派成功。否则,它将撤销交易并不做任何更改。这很重要,因为delegatecall函数不关心委派是否成功;它只会通知您让您做出决定,因为它是一个低级函数,可在许多高级情况下使用。

delegatecall() 的另一个有趣属性是,接收合约的msg.sender将是您的以太坊地址,而不是代理合约的地址。这很重要,否则,在可升级合约中,您的地址将会不同。在许多情况下,用户地址对于某些逻辑很重要,你不希望出现这种情况。

最后,我们有这两个函数:

    function upgradeStorage(address _newStorage) public onlyOwner {
        require(storageAddress != _newStorage);
        storageAddress = _newStorage;
        listStorage.push(_newStorage);
    }

    function upgradeUpgradable(address _newUpgradable) public onlyOwner {
        require(upgradableAddress != _newUpgradable);
        upgradableAddress = _newUpgradable;
        listUpgradable.push(_newUpgradable);
    }

它们只是用于更新智能合约。每当部署新版本的UpgradableStorage时,您都必须执行相应的函数,以使代理合约知道哪个是活动的版本,以便它可以将所有函数调用委托给合约的正确版本。

可扩展的智能合约。

通常,智能合约严格受到区块链处理能力的限制,因为数十万个节点必须相互连接以执行相同的交易。当我们处理超出区块链容量的热门应用程序时,这是一个重大问题。为了解决这个问题,有不同的扩展解决方案,您可以在智能合约中实施,以便许多更多的用户能够运行您的去中心化应用程序而无性能问题。

要理解您将要学习的扩展解决方案,了解使用智能合约时发生的过程很重要:

  • 首先,用户通过运行一个与该合约交互的去中心化应用程序或直接使用他们自己的以太坊节点执行智能合约,为特定智能合约生成一个交易。

  • 矿工接收交易,以便将其包含到下一个区块中并执行字节码。

  • 当他们确认时,交易将永久添加到最长的链中。

我们无法加快矿工确认交易的过程,因为以太坊使用的共识方案要求每个节点验证每个交易,直到它们都变为有效为止。

相反,我们可以使用一种被称为状态通道的协议。它由产生有效的离链交易的承诺方案组成,只要一组人对这些交易达成协议。例如,想象一下像 21 点这样的牌游戏。如果你对这个游戏不熟悉,它是每个玩家轮流接收一张牌,并在获得最大组合牌面的同时下注筹码的游戏。组合总数为 21 的人,或者接近 21 的,赢得游戏。如果超过 21,就会输掉游戏。

本质上,状态通道就像 21 点游戏一样,用户可以玩无数次游戏,同时保持可以在结束后兑换成真实货币的筹码得分。这是一个伟大的扩展系统,因为你可以直接点对点地在链下运行许多交易,而不必等待区块链处理每一笔交易。当你结束游戏时,你只需将结果捆绑在一起,并将一个大交易推送到区块链上。

因此,你会实时处理数百笔交易,直到结束游戏时,你只需发送 1 笔交易。与等待 100 笔交易每笔大约 20 秒不同,你只需要等待 20 秒一次,同时节省了 gas。因此,你拥有了一个更快速且可扩展系统,让更多的玩家能够以更好的性能使用你的 dApp。

使用状态通道实现可扩展的智能合约

我将向你介绍一个简单的示例项目,让你看看状态通道是如何运作的。它们使用的是承诺方案,这只是你不能改变的加密交易,用于在玩家决定展示他们的牌之前承诺你的行动。

智能合约将成为状态通道的进入和退出点。在区块链的控制之外,中间发生的事情,尽管我们将定义每条加密消息里的内容,以便在游戏结束时能够验证它。以下是步骤:

  1. 参与游戏的每个玩家必须通过调用智能合约中的一个函数来打开状态通道,以定义他们是谁。当他们这样做时,他们必须向智能合约发送一些以太作为存款。当游戏结束时,这些资金将用于分发奖励。

  2. 用你想提交的变量生成哈希。这些哈希将包含每个玩家的加密参与。因此,他们通过首先承诺他们的赌注,然后揭示他们的结果并更新变量和余额,直接来回交换消息。

  3. 当他们决定结束游戏时,他们会回到智能合约上载他们最新签名的哈希,以便智能合约可以读取并向每个人发送相应的以太币。

让我们创建带有进入和退出函数的智能合约。其余操作将在链下进行。与往常一样,以下是合约的结构:

pragma solidity 0.5.0;

contract StateChannel {
    constructor () public payable {}

    function exitStateChannel() public {}
}

构造函数将是入口,因为我们想为每场游戏部署一个新的合约实例。exitStateChannel()函数将检查最新签名的哈希,并将相应的以太币发送给每个玩家。请注意,构造函数是可支付的,因为我们希望在开始时收到托管资金。

让我们添加一些我们需要的变量。在这种情况下,我们将创建一个简单的猜数字游戏,每个玩家必须设置一个从 1 到 10 的数字,另一个玩家必须猜出那个数字才能赢。所以,当玩家 2 猜到与玩家 1 不同的数字时,玩家 1 获胜。他们每次猜数字游戏还会打赌以太币;获胜者将以两倍的赌注赢得比赛,只要两者都有足够的资金进行下注:

pragma solidity 0.5.0;

contract StateChannel {
    address payable public playerOne;
    address payable public playerTwo;
    uint256 public escrowOne;
    uint256 public escrowTwo;

    constructor () public payable {}

    function exitStateChannel() public {}
}

接下来,我们需要为每个玩家设置这些变量:

pragma solidity 0.5.0;

contract StateChannel {
    address payable public playerOne;
    address payable public playerTwo;
    uint256 public escrowOne;
    uint256 public escrowTwo;

    constructor () public payable {
        require(msg.value > 0);

        playerOne = msg.sender;
        escrowOne = msg.value;
    }

    function setupPlayerTwo() public payable {
        require(msg.sender != playerOne);
        require(msg.value > 0);

        playerTwo = msg.sender;
        escrowTwo = msg.value;
    }

    function exitStateChannel() public {}
}

正如您所见,构造函数将初始化第一个玩家的地址和托管账户,而setupPlayerTwo()函数将为第二个玩家执行相同的操作。这是我们在两个玩家之间打开状态通道所需要的一切。在创建exitStateChannel()函数之前,该函数将结束游戏,关闭通道并分配资金,我想澄清每个散列消息由哪些元素组成,以及游戏将如何在链下进行。

在这个猜数字游戏中,我们有两个玩家,他们有两个不同的托管账户,每场游戏都在其中下注一个特定的数字。如果两者选择的数字相同,则第二个玩家获胜;否则,第一个玩家获胜。只要他们两者都有足够的资金,他们就会得到两倍数量的以太币投资。

因此,玩家 1 将创建一个包含以下值的加密对象来开始一场游戏:

  • 选择的数字:这个数字必须在 1 到 10 之间。

  • 游戏赌注以太币数量:另一个玩家必须有足够的资金支付你的两倍投资。例如,玩家 1 有 10 个以太币,而玩家 2 有 4 个。鉴于此,如果他赢了,玩家 1 将无法下注超过 4 个以太币,因为如果他赢了,第二个玩家将不得不支付他所有的以太币。我们将保留此选项,以便他们可以投资任意数量,如果赌注超过了玩家的余额,他将简单地获得所有资金,即使它少于赌注。

  • 游戏序列:每场游戏都会增加的计数器,以标识每场游戏的顺序。

  • 时间戳:每个玩家都有一个唯一的时间戳,以了解下注何时进行。

  • 随机数:一个随机的 10 位数字,以确保每个消息都是唯一的。这不是强制性的,但在尝试理解游戏事件的顺序时会有所帮助。

这是一个示例:第一个玩家在与另一个玩家创建智能合约打开状态通道后开始游戏。然后,他创建以下数据将被加密:

  • 选择的数字:9

  • 下注的以太币数量:他在托管账户中有 10 个以太币,所以他将为这场比赛下注 4 个以太币

  • 游戏序列:1

  • 时间戳:1549378379

  • 随机数:2948372910

然后,他使用 dApp 用 keccak256 算法加密该信息:

keccak256(9, 4, 1, 1549378379, 2948372910);

他得到了这个结果的哈希值:

515e473c03c2d08f92825bad975ff0123f15b3ee2f457942a3484abe749f65b4

请注意,这只是一个示例,真正的 keccak256 将会不同。然后,他使用在状态通道智能合约中使用的以太坊账户对该哈希进行签名。他可以使用 web3.js 进行此操作,因为它是与 MetaMask 结合使用在链下最快的签署哈希的方式:

web3.personal.sign(hash, web3.eth.defaultAccount, (err, result) => {
    if(err) return err
    return result
})

之后,加密的哈希将被签名,以后确认他确实选择了那些参数与他的账户。另一个玩家将采取相同的步骤生成加密签名的哈希与他的赌注。

当生成两个哈希时,他们将使用运行某种通信系统的去中心化应用,例如服务器或电子邮件,来交换这些哈希并揭示他们的赌注。他们可以随时验证这些哈希,因为一旦知道了加密信息的有效内容,就可以快速验证该信息是否有效。

每个新消息都会有一个增加的序列号、一个随机的随机数和一个新的时间戳,该时间戳必须大于上一个时间戳。所有这些检查都可以在实现状态通道合约的 dApp 中完成。

当他们决定结束游戏时,他们将上传他们的最新消息到智能合约,以便从在打开通道时配置的托管中分发更新后的余额。以下是如何在智能合约中使用 exitStateChannel() 函数关闭游戏。

首先,我们设置游戏所需的变量以跟踪余额:

pragma solidity 0.5.0;

contract StateChannel {
    address payable public playerOne;
    address payable public playerTwo;
    uint256 public escrowOne;
    uint256 public escrowTwo;

    // Variables to end the game
    uint256 public betOne;
    uint256 public betTwo;
    uint256 public balanceOne;
    uint256 public balanceTwo;
    uint256 public callOne;
    uint256 public callTwo;
    bool public isPlayer1BalanceSetUp;
    bool public isPlayer2BalanceSetUp;
    uint256 public finalBalanceOne;
    uint256 public finalBalanceTwo;
}

然后,我们创建设置函数,这种情况下是构造函数和 setupPlayerTwo(),它的作用是存储用户的初始数据:


constructor () public payable {
    require(msg.value > 0);

    playerOne = msg.sender;
    escrowOne = msg.value;
}

function setupPlayerTwo() public payable {
    require(msg.sender != playerOne);
    require(msg.value > 0);

    playerTwo = msg.sender;
    escrowTwo = msg.value;
}

最后,我们添加最重要的函数:根据最后的状态选择获胜玩家并结束游戏的函数:

function exitStateChannel(
 bytes memory playerMessage, 
 uint256 playerCall, 
 uint256 playerBet, 
 uint256 playerBalance, 
 uint256 playerNonce, 
 uint256 playerSequence, 
 address addressOfMessage) 
 public 
{
    require(playerTwo != address(0), '#1 The address of the player is invalid');
    require(playerMessage.length == 65, '#2 The length of the message is invalid');
    require(addressOfMessage == playerOne || addressOfMessage == playerTwo, '#3 You must use a valid address of one of the players');
    uint256 escrowToUse = escrowOne;

    if(addressOfMessage == playerTwo) escrowToUse = escrowTwo;

    // Recreate the signed message for the first player to verify that the parameters are correct
    bytes32 message = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", keccak256(abi.encodePacked(playerNonce, playerCall, playerBet, playerBalance, playerSequence))));
    bytes32 r;
    bytes32 s;
    uint8 v;

    assembly {
        r := mload(add(playerMessage, 32))
        s := mload(add(playerMessage, 64))
        v := byte(0, mload(add(playerMessage, 96)))
    }

    address originalSigner = ecrecover(message, v, r, s);
    require(originalSigner == addressOfMessage, '#4 The signer must be the original address');

    if(addressOfMessage == playerOne) {
        balanceOne = playerBalance;
        isPlayer1BalanceSetUp = true;
        betOne = playerBet;
        callOne = playerCall;
    } else {
        balanceTwo = playerBalance;
        isPlayer2BalanceSetUp = true;
        betTwo = playerBet;
        callTwo = playerCall;
    }

    if(isPlayer1BalanceSetUp && isPlayer2BalanceSetUp) {
        if(callOne == callTwo) {
            finalBalanceTwo = balanceTwo + betTwo;
            finalBalanceOne = balanceOne - betTwo;
        } else {
            finalBalanceOne = balanceOne + betOne;
            finalBalanceTwo = balanceTwo - betOne;
        }

        playerOne.transfer(finalBalanceOne);
        playerTwo.transfer(finalBalanceTwo);
    }
}

当我们有这么多参数的函数时,将每个参数分开放在新行中是很好的实践。首先,我们检查地址是否已设置,因为我们希望单独设置每个消息。然后,我们重新生成加密的、签名的哈希消息以验证传递的参数是否有效。要验证 keccak256 是否有效,我们只需再次使用假定使用的参数生成它,并检查结果哈希是否与给定的哈希完全相同。

之后,我们使用汇编语言获取 r、v 和 s,这三个变量用于使用您的以太坊账户对消息进行签名。我们用它来获取签名者的地址以验证消息来自正确的人。然后,我们在状态变量中设置该玩家的余额、赌注和调用。我们这样做是为了在第二个玩家使用他的参数执行退出函数时稍后分发资金。最后,在两者都设置好他们的结果消息后,我们使用 transfer() 函数执行以太币转账。

这看起来很复杂,但我们所做的一切只是检查签名消息是否有效,然后检查消息的签名是否有效,最后在其他玩家使用此函数验证退出消息时更新变量以分配资金。

这个智能合约只是一个演示。它有一些重要问题,比如一个玩家可以决定不发布他的退出通道消息,以避免失去以太币。为此,如果第二个玩家在 24 小时内不回应,则需要添加某种冲突解决机制来分配资金。我会把这留给你来练习你的技能。

这些是下一代智能合约,将支持同时在数十万台计算机上运行的复杂去中心化应用程序,并具有一些集中化逻辑。最终,关键在于确保我们利用区块链的无信任性质创建可以使用户受益的应用程序,而无需外部实体干预流程。继续阅读,了解更多使用最少资源获取最大结果的高效智能合约。

油气和数据高效的智能合约

制作智能合约不仅要以透明和安全的方式处理交易,而且要尽可能有效地使用区块链资源。因此,我们将探讨如何为以太坊编写油气和数据高效的代码。在本节中,我们将介绍以下主题:

  • 制作油气高效的智能合约

  • 制作数据高效的智能合约

制作油气高效的智能合约

我相信你已经看到了一些包含许多函数的智能合约,感觉它们消耗了太多的油气以运行:例如那些管理大量状态变量或同时有几个不同合约操作的函数。

尽管区块链要求对每笔交易支付油费,但我们最好还是创建尽可能少消耗资源的智能合约,以便为我们的用户大大降低交易成本。

那么,我们该怎么做呢?

原来每个小操作都有一个与 EVM 关联的操作码。每当使用操作码时,您都需要支付油费,因为矿工必须处理您的交易。以下是最常见操作码及其油费成本的列表:

  • ADDSUB:加或减两个数字。每次添加或减少一个数字时,都会消耗 3 油费。

  • MULDIV:乘以或除以数字。每次操作都会消耗 5 油费。

  • ANDORXOR:用于比较布尔值的逻辑操作。每次操作消耗 3 油费。

  • LTGTSLTSGTEQ:用于比较数字的逻辑操作。每次操作消耗 3 油费。

  • POP:这是一种从堆栈机中删除元素的 EVM 堆栈操作。每次POP操作消耗 2 油费。

  • PUSHDUPSWAP:将元素添加到堆栈机器。这使用了 3 个气体。

  • MLOADMSTORE:向 EVM 的内存中添加数据。每次操作使用 3 个气体。

  • JUMP:跳转到汇编代码中的另一个位置。这需要 8 个气体。

  • JUMPI:有条件的跳转。每次操作需要花费 10 个气体。

  • SLOAD:另一个将元素添加到堆栈的操作。每次操作需要花费 200 个气体。

  • SSTORE:直接将信息存储到区块链中。每次操作需要花费 5000 至 20000 个气体。

  • BALANCE:获取以太坊账户或合约的余额。这每次操作使用 400 个气体。

  • CREATE:部署新的智能合约或账户。我们需要 32000 个气体来创建一个新的智能合约。

正如您所见,将信息存储在状态变量中使用了SSTORE,这会花费高达 20k 个气体。这非常昂贵,是优化代码的一个地方。

这里是我们将探讨并在实际示例中检查真实的气体成本的主要优化技术:

  • 比较运算符的顺序&&||比较运算符的顺序,其中始终检查第一个条件,而第二个条件可能不会被检查。例如,如果 if 语句的第二部分比其他部分更有可能为真,则最好将第二部分放在首位,因为在OR语句中,这将节省气体,因为它不会在检查其他参数时进行不必要的计算。对于AND也是如此:您希望将第一部分作为最有可能为假的部分,因为如果第一部分为假,编译器将不会检查第二部分,您将节省气体,因为每个运算符都有一个气体成本。

  • 使用状态变量的 for while 循环:当您有一个使用外部变量的循环时,您希望它使用内存变量,因为它们比使用存储更便宜得多。

  • 不可访问的 if...else 语句:有些情况下,条件if...else永远不会执行其他部分。在这些情况下,重要的是要删除重复的、多余的和不可访问的代码,因为每次执行都会消耗气体。

  • 限制变量的大小:每个变量类型后面的数字定义了其容量,因此,具有较小容量的变量消耗的气体较少,简单来说,因为它不需要太多的区块链存储:例如,对于较小的变量,使用uint8而不是uint256

  • 尽量不要使用库:每次调用库或外部合约时,都会浪费气体,因为您必须创建该外部合约的实例,这又会浪费气体。这就是为什么在部署时将所有代码组合到一个大型智能合约中比创建较小的片段更好,尽管对开发而言很好,但对生产而言则不太好。

  • 使用内存而不是存储:函数类型后面的 memory 关键字告诉编译器将该信息暂时存储在本地内存中,而不是写入区块链。

  • 限制余额调用:每当你请求某个账户的余额时,你会花费 400 gas,这在规模较大的合约中可能会变得非常大。此外,请记住如果你要多次请求余额,则将余额存储在内存变量中,因为一旦存储,你就不必再次使用操作码 CREATE。

  • 使用 bytes32 而不是 string:字符串是一种更大的变量类型,因为它可以使用交易的所有 gas 来容纳约 1000 个单词。它本身没有限制;相反,它受到你发送的 gas 的限制。越多的 gas,你就可以存储更大的字符串。然而,最好使用 bytes32,因为它更加紧凑,允许你存储多达 32 个字符,这对于较短的文本(如姓名)是理想的。

让我们看一些可以使用这些技术进行优化的示例:

pragma solidity 0.5.0;
contract BadExample {
    uint256 public myNumber = 0;

    function counter(uint256 _counter) public {
        for(uint256 i; i < _counter; i++) {
            myNumber += 1;
        }
    }
}

正如你在前面的合约中所看到的,我们在 counter() 函数内部运行了一个 for 循环,它只是将 myNumber 状态变量按 _counter 次递增。你能看出可能存在问题的地方吗?主要问题在于,每次循环运行时,我们都会向状态变量写入数据,这意味着我们每次迭代都要花费 5k 到 20k gas,因为 myNumber 变量是一个状态变量。

当使用 _counter = 5 运行函数时,交易成本为 48,180 gas,当使用 _counter = 10 时,成本为 74,625 gas。正如你所看到的,成本很快增加了约 26k gas,这正是我们从 SSTORE 操作码中预期的,因为我们运行了五次更多的迭代。

要修复合约,我们只需创建一个新的使用内存而不是存储的本地变量:

pragma solidity 0.5.0;
contract GoodExample {
    uint256 public myNumber = 0;

    function counter(uint256 _counter) public {
        uint256 internalCounter = 0;    
        for(uint256 i; i < _counter; i++) {
            internalCounter += 1;
        }
        myNumber += internalCounter;
    }
}

在这种情况下,交易成本从 27,331 增加到 27,681,仅相差 350 gas。正如你所看到的,这是一个巨大的改进,将在成千上万的交易中至少节省一倍的费用。创建经过优化的 gas 合约是有意义的。这个例子可以通过将 uint256 更改为 uint8 或等效类型来进一步改进,以存储更小的变量。

让我们看另一个例子:

pragma solidity 0.5.0;
contract Example {
    function doSomething() public {
        if (conditionOne && conditionTwo) {
            // Do something
        }
        if (conditionTwo || conditionOne) {
            // Do something
        }
        if (alwaysTrue) {
            // Do something
        } else {
            // Do another thing
        }
    }
}

这个函数运行一组带有不同条件的条件语句。在第一个情况中,即 if (conditionOne && conditionTwo),我们可以通过将最有可能为假的条件放在首位来改进代码,因为这样可以节省 gas,不进行第二个条件的不必要检查。&& 运算符在第一个条件为 false 时不会考虑第二个条件,因为它要求两个条件都为 true,这是有意义的,因此它节省了 gas。考虑以下情况,我们定义了每个条件的可能性:

  • conditionOne 80% 的时间将为 true,这意味着在 100 次函数调用后,它将为 true 80 次

  • conditionTwo 在 20% 的情况下为真,这意味着经过 100 次函数调用后,它将为真 20 次。

如果我们保持函数不变,并且运行该函数 100 次,那么每次第一个条件为真但另一种情况为假时,我们将浪费 3 gas。请记住,运行 AND (&&), OR (||), 和 XOR (^) 操作需要 3 gas。所以,经过 100 次运行后,我们将浪费 80 次 && 检查,因为在这些情况下第二个条件为假,这意味着我们将浪费 300 gas。这似乎不多,但它会累积到许多不同的函数和交易中,因此务必注意条件语句的顺序。

正确的版本应该是如下所示:

pragma solidity 0.5.0;
contract Example {
    function doSomething() public {
        // Notice the condition 2 going first because it will be false most of the times, thus rending the second condition unnecessary to check
        if (conditionTwo && conditionOne) {
            // Do something
        }
    }
}

对于 OR || 语句,情况也是一样的,我们希望先放置最有可能为真的条件,因为我们只需要其中一个为真。每当第一个条件为真时,编译器将停止检查该 if 语句中的其余条件以节省 gas,因为这是不需要的。考虑到前面示例中相同的概率,其中 conditionOne 80% 的时间为真,而 conditionTwo 20% 的时间为真,我们可以修复代码以优化 gas:

pragma solidity 0.5.0;
contract Example {
    function doSomething() public {
        // Notice the condition 1 going first because it will be true most of the times, thus rending the second condition unnecessary to check
        if (conditionOne || conditionTwo) {
            // Do something
        }
    }
}

函数中的最后一个条件语句是这样的:

if (alwaysTrue) {
    // Do something
} else {
    // Do another thing
}

因为 if 语句中的条件将始终为真,所以添加 else 块是没有意义的,因为它永远不会运行。在这种情况下,删除该 else 块以节省处理函数时的 gas 是有意义的:

pragma solidity 0.5.0;
contract Example {
    function doSomething() public {
        // Notice how we removed the } else { block because it will never be execute
        if (alwaysTrue) {
            // Do something
        }
    }
}

这种检查对于覆盖测试也很重要,因为它使用工具来验证代码的所有部分是否至少被触及一次,以移除不必要的元素。

作为本节的最后一条建议,我想让你了解一种非常高效存储信息的小技巧:使用索引事件作为存储。事件在区块链中有一个专门的部分,执行成本要低得多,因此它们非常节省 gas。这可以用来存储你想要在你的 dApps 中使用的小字符串和变量,成本最小化。你可以稍后使用 web3.js 找到每个事件,因为它们是被索引的,这意味着它们可被搜索到特定参数。另外,请注意,每个事件只能有三个被索引的参数,因此你可以创建多个不同的被索引的事件。

记住,每次编写合约时都要回到我们之前列出的 8 点清单,以确保高效的智能合约流程,节省人们的资金并减少区块链的大小,这正是我们所有人想要的。

最后一条建议是,您可以为可能昂贵的功能创建viewpure函数,因为pureview函数不消耗任何气体,因为它们在本地处理计算,所以您可以免费使用计算。例如,如果您想在 Solidity 中对数组进行排序,您将不得不花费大量的气体,因为每次迭代都会花费气体,但是,如果排序函数是view,则不会花费任何气体,因此您可以排序任意数量的数组而不花费气体。

制作数据高效的智能合约

什么是数据高效的智能合约?就是易于阅读、理解和管理的代码。状态变量是数据高效性讨论时的主要组成部分:我们希望优化代码的可维护性。这不仅对开发者之间解释代码很重要,而且对于想要调查智能合约代码实际操作的客户也很重要。正确设置的数据结构将节省人们数小时的头痛,并且将指数级提高代码质量。

Vyper 在这方面做得非常出色,因为您可以使用专门设计的自定义单元类型来提高可读性。它还使用了从 Python 继承的最小语法系统,有助于提高代码的可读性。一个数据高效的智能合约是什么样子?看看这个例子:

pragma solidity 0.5.0;
contract GoodExample {
    // The number of seconds that you have each game to make decisions up to 100
    uint8 public secondsPerGame;

    // To check if owner's address is setup when executing restricted functions
    bool public isOwnerAddressSetup;

    // The name of the first player
    bytes32 public firstPlayersName;
}

正如您所看到的,每个变量上方都有一个简短的注释,以帮助您理解它应该做什么,因为发现可能会因错误原因而被误解的令人困惑的变量并不罕见。每个名称都以简明的命名适当地定义,试图尽可能清楚地解释变量的用途,就像它们提供了关于何时不使用它们的明确理解一样清楚。

变量类型是每个变量目的的正确类型,因为我们希望通过最大化虚拟机的功能来使它们高效使用气体,而不需要不必要地花费大量气体。

相比之下,一个糟糕的合约会是这样的:

contract BadExample {
    uint256 numberOfTimes;
    string public name;
    address public senderAddress;
}

在这里,命名过于简单,留下了太多的混乱空间。每个变量上方没有注释帮助您理解它们应该何时使用,用于什么目的以及有什么限制。变量类型也可以改进:与其为名称使用string,也许使用bytes32更合理,因为对于短文本来说,我们不需要如此大的类型。对于uint256,同样的情况也是如此,我们必须考虑减少其大小是否更好。

总的来说,我们希望在我们的合约中提供尽可能多的澄清。为此,我们将在每个变量的顶部编写出色的描述,即使我们认为它已经足够清楚了;我们将有效地使用类型来优化气体成本;我们将简明地命名变量以帮助即将到来的开发者理解合约代码的复杂性。

许多大型智能合约变得过于庞大,难以轻松理解,因此,对数据效率进行优化对您来说至关重要,因为这是一项重要的预防安全措施,可用于检测危险的入口点。

您刚刚学会了如何通过正确理解与其相关的成本的操作码来优化您的智能合约,以便在创建将由数千其他人执行的函数时,可以做出更好的决策,从而节省人们的以太币。

智能合约安全分析

在去中心化应用中的安全性是必须要仔细处理的问题,因为我们正在处理来自信任代码足够安全以安全保管他们资金的真实人的真实资金。您不能忽略代码中的安全分析,否则将会冒着风险损害他人的资金,因此这是一项巨大的责任。

这也是保护智能合约可能如此耗时和昂贵的主要原因之一。对于 ICO 智能合约和处理真实资金的合约来说,审计、漏洞赏金和代码分析是常见的。它们自然昂贵,因为需要仔细考虑智能合约中所有运动部件。

保护智能合约的技术

在部署智能合约到主网之前,让我们来看看一些常见的检查事项:

  • 溢出和下溢:溢出意味着超出其容量,从而将其值重置为从零重新开始计数。相反,下溢是从负面超出数字容量,所以当您超出uint类型变量的零时,该值会直接跳到该变量的最大值。例如,假设您想在 uint8 中存储 5000:您能安全地做到吗?不,因为 uint8 的最大数是 2 ** 8,即 255(不是 256,因为我们从零开始),因此在存储未知值之前,您将多次超出该类型的容量。如您所知,uints不能为负,因此当您尝试在其中存储负数时,它们将下溢。注意这些情况并在适当的位置进行检查,以避免超出数字类型的容量。

  • 文档:不记录代码并不是一个安全风险,因为文档更多地与您节省时间的事实有关,因为不会混淆可能被错误解释的函数。当您正确记录您的代码时,您可以快速了解它可能失败的地方,这使得在审计代码时更容易维护和更安全。出于这个原因,我始终建议使用 NatSpec 文档,这是一种使用一些常见参数描述您的函数的方法,这些参数可以很容易地被编译器和开发人员理解。

  • 重入攻击:你可能熟悉这种攻击,它利用delegatecall函数调用外部合约以恶意方式更新它们。这是非常危险的,必须在看到低级函数如delegatecall时仔细分析。你可以通过限制对最重要功能的访问,通过可见性或修饰符来避免它。

  • 竞争条件:这种类型的攻击利用了燃气限制来重复运行特定代码序列,直到燃气耗尽。可以通过确保在增加状态变量之前减少它们来预防这种情况。例如,对于一个代币合约,每当你用以太币购买代币时增加你的余额,重要的是在增加你拥有的代币余额之前减少你拥有的以太币数量,以避免重新进入情况。

有许多与每个单独合约相关的小型安全问题。要修复它们,你需要慢慢分析你的智能合约,找到可能导致潜在风险的地方。

摘要

在本章中,你首先通过一个版权内容项目市场来提高你的智能合约开发技能,以保护用户的内容。然后,你深入探讨了智能合约世界的深处,了解了它的内部工作原理以及智能合约如何从这一美妙的技术中受益。之后,你了解了以太坊中最强大的技能之一:开发可升级和可扩展的智能合约,这些合约可以用于高级项目,以便你可以高效地运行大型企业的大规模应用程序。

接下来,你了解了数据和燃气在智能合约中的流动,这样你就能更好地管理区块链限制内可用的宝贵资源的消耗,最终以更高质量的合约节省人们的金钱和时间。最后,你对安全性分析来保护你的代码免受恶意攻击,以使人们的资金完全安全在你的 dApps 中。

在下一章中,你将探索先进的去中心化应用程序的奇迹,挑战你对如何从零开始创建出伟大的 dApps 的当前理解。你将逐步了解构建规模化项目的更强大的 dApps 的最有效方法。

第五章:掌握 dApps

在掌握 dApps 中,您将学习如何创建高级别的分散式应用程序,这些应用程序使用我们在前几章中看到的智能合约。我们将从头开始逐步完成所有步骤,包括规划、开发代码和测试应用程序。首先,您将了解 dApp 的结构,以便能够有效地从头开始创建新的 dApp。您将通过安装以太坊和 Truffle 来为您的产品使用它。然后,您将学习如何创建出色的用户界面,展示正确的内容而不会混乱。最后,您将创建与 dApp 交互所需的智能合约,并将这些合约集成到界面中,以便用户可以轻松地与合约交互。

在本章中,我们将涵盖以下主题:

  • 介绍 dApp 架构

  • 安装以太坊和 Truffle

  • 设置和配置以太坊和 Truffle

  • 创建 dApp

  • 创建用户界面

  • 将智能合约连接到 Web 应用程序

介绍 dApp 架构

架构化分散式应用程序意味着做出高级别的软件决策,以指导我们的想法设计。我们正在规划步骤,以便能够流畅地创建一个 dApp,而不会陷入设计决策中。这也意味着计划智能合约将如何与 dApp 通信,用户将如何与 dApp 交互,以及我们希望最终产品具有哪些功能。

设计应用程序时,我们希望重点关注用户体验,以便他们在使用生成的 dApp 时感到舒适。这就是为什么在我们开始编码之前,拥有清晰的外观愿景非常重要,因为如果我们想要一个现代化的 dApp,以满足技术用户的需求,我们将不得不更多地关注提供有关应用程序每个元素的详细信息。

例如,假设您想创建一个博客 dApp,用户可以在其中发布关于特定主题的文章。您将如何设计这样一个 dApp?您从何开始?事实上,没有一种完美的系统可以从头开始设计您理想的 dApp;这更像是一个交互式过程,您需要反复回到草图板上澄清您的想法,随着开发的进行。

就个人而言,我喜欢从我脑海中对所想到的内容进行详细描述,尽可能多地澄清,并列出必须始终存在的功能列表。例如,我想为享受阅读有关新项目并通过 ICO、TGE 或 STO 等筹款的 ICO 爱好者创建一个分散式博客,因为他们处于相同的生态系统中,周围有联合风险投资、股权支付、区块链创新等。这个博客将奖励用户的代币,他们将能够在系统中交换以获得奖励。可用于交换代币的高可见性、每篇文章的高级度量标准、高级文章和投票决策。以下是此 dApp 的功能:

  • 按标题和标签查找文章的搜索系统

  • 具有实时响应的评论区,就像聊天一样

  • 作者的写作工具

  • 提升文章可见性的推广工具

  • 一个可见度评分,指示每篇文章在此 dApp 内容海洋中的可见性

  • 设计工具来自定义每个评论和发布的外观

然后,将有助于绘制一个关于构成整个 dApp 的组件之间关系的图表——不同的后端服务、前端和区块链交互。这个想法是让你以视觉方式看到你应用程序中的重要内容。你可以包含任何你认为将对最终 dApp 有关的信息,以便提醒你什么是重要的,什么不重要。你会发现自己自动地丢弃那些不重要的东西。看看下面的图表:

你可以在技术方面做得更深入,但在创意设计阶段,这并不是那么重要,你的目标是填补你计划创建的内容空白。这会在你意识到之前显示出对你来说重要的东西。然后,你可以创建一个你的智能合约将拥有的数据结构类型的方案,你用户界面的构建模块,以及服务器性能特性,以提供迅捷的交互。

然后,重要的是你问自己几个设计问题,比如以下几个:

  • 我们将如何处理用户数量的突然增加?

  • 如果我们面对像 DDoS 这样的攻击,必须采取哪些措施来保持应用程序的运行?

  • 从用户的角度看,成功的交互是什么样的?

  • 我们认为我们将面临不确定性的地方在哪里,有哪些事情还不够清晰?

  • 所有核心功能完成的现实日期是什么?

  • 这个想法的最小可行产品MVP)是什么样子?哪些功能是必不可少的?

  • 我们什么时候能完成 MVP?

  • 我们为普通用户解决了什么问题?

  • 我们提供了哪些解决方案,他们在其他地方找不到?

  • 我们如何在开始之前发现或创造对我们想法的需求?

  • 我们能否让早期用户帮助我们提供专门针对这类人的产品?

  • 我们的理想用户是什么样的:他们做什么,他们的爱好是什么,他们在网上待在哪里(以便我们可以将他们纳入到创建更好产品的开发过程中)?

  • 我们的目的是什么,为什么要做这件事?列出背后为什么的三个原因。

  • 我们在哪些方面可以做得更好、更快、更高效?

从这里开始,你可以根据这些想法开始创建你的去中心化应用。务必创建一个免费且易于使用的 Google 表单,以澄清你认为相关的所有问题及更多问题。在回答中要明确和详细。永远记住专注于解决用户存在的问题。成功交付高质量的去中心化应用的关键是有一个坚实的基础,明确你为何这样做,以便在困难时保持动力—这就是为什么问题如此重要。这是从内心获得动力以面对困难任务的诀窍。

让我们回顾一下你到目前为止学到的东西。你学会了用几种心智工具来揭示你想法的薄弱点,以便你可以创建坚实的应用而不陷入设计不良交互中。你明白了优秀问题是清晰思考的核心基础。你希望自己能提出尽可能多的问题,并把它们写下来以记住你的目的,这样你就可以充分激励自己继续并完成你的目标。继续阅读下一节以了解更多有关设置开发环境的信息。

安装以太坊和 Truffle

要创建真正强大的去中心化应用,我们需要有本地版本的以太坊和 Truffle。你不能单独获取以太坊;你需要使用一个客户端,这在这种情况下将是 Geth。Truffle 是一个在你的机器上创建、部署和测试 dApp 的框架,无需等待外部服务,一切都在一个地方。

以太坊有不同的实现方式,最著名的是 Parity 和 Geth。当涉及到安装以太坊时,实际上是获取一个实现其协议的客户端,所以你可以选择你喜欢的开发系统。

让我们继续安装以太坊和 Truffle。首先,在 Mac 上获取以太坊,你需要运行以下命令:

brew update && brew upgrade
brew tap ethereum/ethereum
brew install ethereum

这将在几分钟内为你的 Mac 编译所有需要的代码。要在 Windows 上获取 Geth,你需要从官方网站下载二进制文件:geth.ethereum.org/downloads/;你也可以获取其他系统的二进制文件,但是尽可能在终端中安装 Geth 会更容易和有趣。然后,只需打开 geth.exe 文件运行以太坊。

要在 Linux 上安装以太坊,你必须在终端上执行以下命令:

sudo apt-get install software-properties-common
sudo add-apt-repository -y ppa:ethereum/ethereum 
sudo apt-get update && sudo apt-get upgrade
sudo apt-get install ethereum

现在,要获取 Truffle,你需要执行一些额外的步骤:

  1. 首先,从官方网站nodejs.org安装 Node.js LTS 版本。

  2. 然后,使用文件浏览器打开它以运行安装过程

  3. 安装完成后,运行 node -vnodejs -v;如果不起作用,请验证是否已安装

  4. 运行 npm i -g truffle 安装 truffle

您现在可以通过在空文件夹上运行以下命令来为您的项目使用 Truffle,该命令将为任何truffle项目生成所需的文件结构:

truffle init

设置和配置以太坊和 Truffle

现在我们有了所需的工具,我们将设置基本的文件结构,以便我们有一个干净的环境来处理我们所需的所有 dApps。只要您需要,您就可以一次又一次地使用它,因为它已经设置了所有的依赖项。

首先,让我们创建一个名为dapp的文件夹,其中将包含我们的所有文件。然后,使用您的终端或命令行,在dapp文件夹内执行truffle init以设置 Truffle,确保您在dapp文件夹内。

在该文件夹中安装 Truffle 后,运行npm init来设置 Node.js 的package.json文件,这将允许您安装 npm 插件。它会要求您提供有关您的项目的一些常规信息;只需按照您的意愿填写或按Enter键留空,这是我通常所做的,除非我打算将该项目分发给其他人使用。

您将看到您已创建以下文件夹:

  • contracts/:您的合同将放在这里。现在,它有一个迁移合同,以更新您的合同,因为您改进了您的代码。

  • migrations/:这里是您定义智能合约将如何部署、构造函数将有哪些参数等的地方。

  • test/:您的智能合约和 dApps 的测试将放在这里。

  • package.json:主要的 npm 文件,用于从节点注册表中安装包。

  • truffle-config.js:一个配置文件,用于定义如何连接到区块链,您将使用哪个以太坊帐户等。

安装所需的软件包

现在我们要做的是安装我们将用于使用 React 和 webpack 的基本包。首先,使用以下命令将您的npm版本更新为最新版本:

npm i -g npm@latest

如果您还没有这样做,请进入您的dapp项目文件夹,并使用以下命令安装webpack

npm i -S webpack webpack-cli

Webpack 是一个实用工具,它会将所有 JavaScript 文件合并为一个单一的、庞大的、易于管理的 JavaScript 文件,以便您可以优化开发时间。

安装了 Webpack 后,安装所有babel依赖项。Babel 是一个实用工具,它与 webpack 一起工作,将您的 JavaScript 文件转换为最新版本,以便每个浏览器都与新的 JavaScript 功能兼容,鉴于各种浏览器之间存在重大差异需要进行标准化。Babel 就是这样做的,您可以这样安装它:

npm i -S @babel/core @babel/preset-env @babel/preset-react babel-loader

然后,我们需要安装react.js,因为我们将在我们的项目中使用它,如下所示:

npm i -S react react-dom

设置 webpack

我们现在可以生成webpack.config.js文件,其中我们将指定如何处理我们的 JavaScript 文件,以及组合版本将部署到何处。在您的dapp/文件夹的根级别创建一个空的webpack.config.js文件,并使用以下配置:

const path = require('path')

module.exports = {
    entry: './src/index.js',
    output: {
        filename: 'bundle.js',
        path: path.join(__dirname, 'dist')
    },
    module: {
        rules: [
            {
                test: /\.js$/,
                exclude: /node_modules/,
                use: {
                    loader: 'babel-loader'
                }
            }
        ]
    }
}

模块导出是在node.js项目中使用的导出对象。它包含rules数组,其中你指示哪些文件必须通过哪些编译器,这里是babel-loader。入口和输出属性定义了在合并文件后生成文件的位置。增加 webpack 配置文件中的一些额外信息以定义 HTML 结果文件;这是为了自动生成一个包含你的 JavaScript 文件的有效 HTML 页面。安装以下加载器:

npm i -S html-webpack-plugin html-loader

更新你的 webpack 配置如下:

const html = require('html-webpack-plugin')
const path = require('path')

module.exports = {
    entry: './src/index.js',
    output: {
        filename: 'bundle.js',
        path: path.join(__dirname, 'dist')
    },
    module: {
        rules: [
            {
                test: /\.js$/,
                exclude: /node_modules/,
                use: {
                    loader: 'babel-loader'
                }
            }
        ]
    },
    plugins: [
        new html({
            template: './src/index.html',
            filename: './index.html'
        })
    ]
}

设置源文件夹

让我们看看设置源文件夹的以下步骤:

  1. 创建一个src文件夹,所有你的开发代码都会在这里。现在你的项目设置应该是这样的:

  1. src/中创建一个名为index.html的新文件,使用以下代码:
<!DOCTYPE html>
<html lang="en" dir="ltr">
    <head>
        <meta charset="utf-8">
        <title>Startup</title>
    </head>
    <body>
        <div id="root"></div>
    </body>
</html>
  1. <div id="root">对象将是我们 React 项目的起点。有了 HTML、webpackbabel 设置,我们可以开始创建主react.js文件。在src/文件夹中创建一个名为index.jsx的文件,里面包含我们的初始react.js文件:
import React from 'react'
import ReactDOM from 'react-dom'

class Main extends React.Component {
    constructor() {
        super()
    }

    render() {
        return (
            <div>The project has been setup.</div>
        )
    }
}

ReactDOM.render(<Main />, document.querySelector('#root'))

在这里,我们导入React和 ReactDOM,将 React 与我们的 HTML 文件连接起来。然后,我们创建一个Main类,它有一个简单的构造函数和一个render()函数,该函数返回一个确认项目已正确设置的消息。

  1. 最后,使用以下命令编译这些文件,其中-p表示生产环境:
webpack -p
  1. 记得在你的项目文件夹中执行。编译文件后,你需要运行一个静态服务器,将文件传送到你的浏览器,以便你可以使用你的 dApp。为此,使用以下命令安装http-server
npm i -g http-server
  1. 然后,为你的分发文件运行它:
http-server dist/
  1. 在浏览器中打开localhost: 8080,即可查看你的 dApp 项目实时展示:

恭喜!你现在拥有一个可以复制到其他想要创建的 dApp 的工作启动项目。

你可以将项目发布到你的 GitHub 上,以便在其他情况下需要时克隆它,执行以下步骤:

  1. 打开github.com,点击 New 创建一个新的仓库。命名为dapp,选择 gitignore Node 和 MIT 许可证。这是我创建的一个示例:github.com/merlox/dapp

  2. 现在,回到你的终端并键入git init,如果你的系统上已安装了git。\那将在你的文件夹中启动一个新的 GitHub 项目。

  3. 然后,当你在使用自己的凭证提交新文件时,你需要告诉 GitHub 你想要更新哪个仓库的更改;你可以使用以下命令来永久执行所有这些操作:

git config remote.origin.url https://<YOUR-USERNAME>:<YOUR-PASSWORD>@github.com/<YOUR-USERNAME>/dapp
  1. 用以下命令从你的仓库中拉取初始许可文件:
git pull
  1. 使用git add .添加你的文件,用git commit -m进行提交。首先,提交它们,然后用git push origin master推送它们。接下来,你将在你的新存储库中看到你的文件。

记得不要推送任何新的更改,因为你希望这个存储库在未来使用相同文件结构的项目中保持不变。

创建 dApps

你现在已经准备好用 Truffle, React, Webpack 和 Solidity 创建 dApps。为了尽快获得必要的知识,我们将经历创建一个完全可行的分散式应用程序所需的所有步骤。在本章中,我们正在创建一个音乐推荐社交媒体平台,在这里人们将能够发布他们当时喜欢的歌曲,以帮助他们的朋友找到有趣的音乐来享受,所有这些都存储在智能合约内,而不需要一个集中的服务器。

我们将首先创建智能合约,然后是用户界面,最后,我们将用web3.js将它们全部结合在一起。在主界面完成后,我们将测试我们的分散式应用程序,以确保它正常运作。

创建智能合约

在直接创建智能合约之前,让我们定义我们需要它做什么:

  • 我们需要一个数组,它将以字符串或 bytes32 格式存储每个用户的音乐推荐

  • 一个用于定义用户信息的结构体

  • 一个包含每个用户推荐的映射

  • 一个关注用户的数组,这样我们可以看到新的音乐更新

跟往常一样,我们从创建基本的智能contract结构开始:

pragma solidity 0.5.0;

contract SocialMusic {

}

接下来,我们定义我们将使用的变量,如下所示:

pragma solidity 0.5.0;

contract SocialMusic {
    struct User {
        bytes32 name;
        uint256 age;
        string state; // A short description of who they are or how they feel
        string[] musicRecommendations;
        address[] following;
    }
    mapping(address => User) public users;
}

每个用户结构体将保存该用户做出的所有音乐推荐。现在,我们需要创建函数来添加新的音乐推荐。我们不会有函数来删除或修改过去的推荐,因为我们希望它成为分享过去和现在的音乐品味的永久场所,如下所示:

pragma solidity 0.5.0;

contract SocialMusic {
 struct User {
 bytes32 name;
 uint256 age;
 string state; // A short description of who they are or how they feel
 string[] musicRecommendations;
 address[] following;
 }
 mapping(address => User) public users;

    // To add a new musical recommendation
    function addSong(string memory _songName) public {
        require(bytes(_songName).length > 0 && bytes(_songName).length <= 100);
        users[msg.sender].musicRecommendations.push(_songName);
    }
}

addSong()函数接受歌曲名作为字符串,并将该歌曲推荐到特定以太坊地址的音乐推荐数组中。songName 必须在 1 和100个字符长之间,以避免极大或空的推荐。

然后,我们需要创建用于创建新用户和关注其他用户的函数。人们可以只用他们的地址发布音乐推荐,如果他们不想设置他们的姓名年龄,他们将是匿名的,因此setup函数是可选的:

pragma solidity 0.5.0;

contract SocialMusic {
    struct User {
        bytes32 name;
        uint256 age;
        string state; // A short description of who they are or how they feel
        string[] musicRecommendations;
        address[] following;
    }
    mapping(address => User) public users;

    // To add a new musical recommendation
    function addSong(string memory _songName) public {
        require(bytes(_songName).length > 0 && bytes(_songName).length <= 100);
        users[msg.sender].musicRecommendations.push(_songName);
    }

    // To setup user information
    function setup(bytes32 _name, uint256 _age, string memory _state) public {
        require(_name.length > 0);
        User memory newUser = User(_name, _age, _state, users[msg.sender].musicRecommendations, users[msg.sender].following);
        users[msg.sender] = newUser;
    }
}

setup函数必须至少接收用户的_name,其他参数是可选的,而在设置之前做出的推荐将与该用户保持链接。以下是follow函数的样子:

// To follow new users
function follow(address _user) public {
    require(_user != address(0));
    users[msg.sender].following.push(_user);
}

我们只是将一个新地址推送到以下用户的数组中。你可以使用 Remix 手动测试所有功能来部署你的合约。要使用 Truffle 部署它,我们首先需要设置truffle-config.js配置文件,并确保我们的SocialMusic.sol文件位于项目的contracts/文件夹中。正如你在之前的课程中学到的那样,要为ropsten设置truffle-config.js,我们需要取消注释第 63 行的ropsten对象:

    ropsten: {
      provider: () => new HDWalletProvider(mnemonic, `https://ropsten.infura.io/${infuraKey}`),
      network_id: 3, // Ropsten's id
      gas: 5500000, // Ropsten has a lower block limit than mainnet
      confirmations: 2, // # of confs to wait between deployments. (default: 0)
      timeoutBlocks: 200, // # of blocks before a deployment times out (minimum/default: 50)
      skipDryRun: true // Skip dry run before migrations? (default: false for public nets )
    },

然后,在文件开头的第 21 行取消注释变量:

const HDWalletProvider = require('truffle-hdwallet-provider');
const infuraKey = "fj4jll3k.....";

const fs = require('fs');
const mnemonic = fs.readFileSync(".secret").toString().trim();

将你的infuraKey更改为你的个人密钥,你可以在infura.io创建项目后找到。如果你不知道如何获取infuraKey,请返回到第三章,以太坊资产,并执行以下代码:

const infuraKey = "v3/8e12dd4433454738a522d9ea7ffcf2cc";

创建一个包含你的 MetaMask 助记词的.secret文件,Truffle 将使用它来使用你的第一个以太坊帐户部署你的SocialMusic智能合约。如果你正在进行 git 项目的开发,请确保将.secret添加到你的.gitignore文件中,这样你的帐户就不会泄露给别人看到并窃取你的以太币。

在部署你的合约之前,你需要安装钱包提供程序,以便truffle可以访问你的帐户:

npm i -S truffle-hdwallet-provider

现在,你需要告诉 Truffle 你想要部署哪些合约。你可以通过打开migrations/1_initial_migrations.js文件并相应更改来实现:

const SocialMusic = artifacts.require("./SocialMusic.sol")

module.exports = function(deployer) {
  deployer.deploy(SocialMusic)
}

在设置好秘密助记词之后,为ropsten部署你的合约;记得要有足够的ropsten以太币用于部署:

truffle deploy --network ropsten --reset

--reset标志将强制 Truffle 部署新版本的合约,如果你之前部署的是无效的。Truffle 的好处在于设置好一切之后,你可以非常高效地快速部署新版本的合约进行测试。如果一切都运行成功,你将看到类似于下面的内容:

恭喜!你刚刚部署了你的SocialMusic智能合约。继续阅读,了解如何创建我们将用于与智能合约交互的用户界面。

创建用户界面

因为我们有一个干净的react.js项目正确设置,所以我们可以立即开始创建我们应用程序的用户界面。我们将使用示例数据来检查设计,然后再移动并集成真实的智能合约代码。

打开src/index.js文件开始编写你的设计:

import React from 'react'
import ReactDOM from 'react-dom'

class Main extends React.Component {
    constructor() {
        super()
    }

    render() {
        return (
            <div>
                <h1>Welcome to Decentralized Social Music!</h1>
                <p>Setup your account, start adding musical recommendations for your friends and follow people that may interest you</p>
                <div className="buttons-container">
                    <button>Setup Account</button>
                    <button>Add Music</button>
                    <button>Follow People</button>
                </div>
                <h3>Latest musical recommendations from people using the dApp</h3>
                <div ref="general-recommendations"></div>
            </div>
        )
    }
}

ReactDOM.render(<Main />, document.querySelector('#root'))

我们将设计写在render()函数内部,因为这是所有代码将显示给用户的地方。我创建了两个主要部分:一个h1欢迎用户来到 dApp,并显示给他们三个按钮开始使用它的简短消息,以及一个h3部分向用户展示网络上随机人员最新的 10 条音乐推荐:

为了改善应用程序的外观,我们将使用一些基本的 CSS,让用户感觉很棒。在src/目录下创建一个名为index.css的新文件。为了能够在我们的react.js应用程序中使用 CSS,我们需要使用一个新的加载程序来理解 CSS。打开你的webpack.config.js文件,在 rules 块中添加以下部分,就像之前使用其他加载程序一样:

{
    test: /\.css$/,
    exclude: /node_modules/,
    use: [
        {loader: 'style-loader'},
        {loader: 'css-loader'}
    ]
}

然后,安装css-loaderstyle-loader,如下所示:

npm i -S style-loader css-loader

现在,我们可以在index.css中编写我们的 CSS 代码,如下所示:

body {
    margin: 0;
    font-family: sans-serif;
    text-align: center;
}

button {
    border-radius: 10px;
    padding: 20px;
    color: white;
    border: none;
    background-color: rgb(69, 115, 233);
    cursor: pointer;
}

button:hover {
    opacity: 0.7;
}

.buttons-container button:not(:last-child){
    margin-right: 5px;
}

你应该得到类似以下截图的东西:

现在,我们需要实现每一个这些功能,但在此之前,让我们先创建一个名为推荐的新组件,它将是一个包含某个用户推荐的个人音乐推荐的个体盒子。它将是这个样子:

import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'

class Main extends React.Component {
    constructor() {
        super()
    }

    render() {
        return (
            <div>
                <h1>Welcome to Decentralized Social Music!</h1>
                <p>Setup your account, start adding musical recommendations for your friends and follow people that may interest you</p>
                <div className="buttons-container">
                    <button>Setup Account</button>
                    <button>Add Music</button>
                    <button>Follow People</button>
                </div>
                <h3>Latest musical recommendations from people using the dApp</h3>
                <div ref="general-recommendations">
                    <Recommendation
                        name="John"
                        address="0x5912d3e530201d7B3Ff7e140421F03A7CDB386a3"
                        song="Regulate - Nate Dogg"
                    />
                </div>
            </div>
        )
    }
}

class Recommendation extends React.Component {
    constructor() {
        super()
    }

    render() {
        return (
            <div className="recommendation">
                <div className="recommendation-name">{this.props.name}</div>
                <div className="recommendation-address">{this.props.address}</div>
                <div className="recommendation-song">{this.props.song}</div>
            </div>
        )
    }
}

ReactDOM.render(<Main />, document.querySelector('#root'))

我们新增了一个显示三个包含推荐歌曲的名称、地址和歌曲的 div 的新组件。我还在Main组件中添加了一个示例用法,这样你就可以看到它是如何工作的。Props 只是你从一个组件传递到另一个组件的变量,并且它们是通过它们的变量名识别的。让我们用一些 CSS 代码改善一下外观,如下所示:

.recommendation {
    width: 40%;
    margin: auto;
    background-color: whitesmoke;
    border-radius: 20px;
    margin-bottom: 10px;
    padding: 40px;
}

.recommendation-name, .recommendation-address {
    display: inline-block;
    color: #444444;
    font-style: italic;
}

.recommendation-name {
    margin-right: 10px;
}

.recommendation-address {
    color: rgb(156, 156, 156);
}

.recommendation-song {
    font-weight: bolder;
    font-size: 16pt;
    margin-top: 10px;
}

这就是我们刚刚做的改变和一些更多音乐推荐样本的样子:

就是这样——你刚刚创建了你的去中心化SocialMusic平台的 UI。让我们通过集成web3.js使它动态起来,以便我们可以使用我们的智能合同让人们与它互动。

将智能合同连接到 web 应用程序

去中心化应用程序由智能合同和用户界面组成。我们现在有了这两个元素,我们可以通过使用web3连接前端和后端来将它们结合起来,web3是从浏览器与以太坊区块链交互的最强大工具。

让我们从获取web3.js开始。你可以用以下命令来安装它:

npm i -S web3

然后,将它导入到你的index.js文件中,如下所示:

import React from 'react'
import ReactDOM from 'react-dom'
import web3 from 'web3'
import './index.css'

你不需要安装web3,因为大多数用户(如果不是所有用户)都会安装 MetaMask,它会自动将web3.js注入到你的应用程序中。尽管如此,在应用程序的代码中有web3是一个好习惯,因为你可以控制应用程序中使用的版本。

要将我们的react.js应用程序完全连接到我们的SocialMusic合同,我们需要实现合同的每个函数,以便它们可以从我们设计的用户界面执行。我们还想要检索特定信息,比如最后推荐的五首歌曲。你可以用许多方法开始实现你的合同,所以,我们将首先让我们 web 应用程序中的所有三个按钮与智能合同正常工作。

设计设置表单

首先,我们有“设置账户”按钮。这个按钮应该向用户显示一个表单,其中包含几个输入来设置他们的姓名、年龄和状态,其中只有姓名是必填的。还应该有一个取消按钮和一个提交按钮。让我们创建一个新的 React 组件,我们将其称为Form,它将包括所有这些要求:

class Form extends React.Component {
    constructor() {
        super()
    }

    render() {
        return (
            <form className={this.props.className}>
                <input type="text" ref="form-name" placeholder="Your name" />
                <input type="number" ref="form-age" placeholder="Your age" />
                <textarea ref="form-state" placeholder="Your state, a description about yourself"></textarea>
                <div>
                    <button>Cancel</button>
                    <button>Submit</button> 
                </div>
            </form>
        )
    }
}

我们添加了两个带有文本区域的输入,用于状态和两个按钮以取消或提交。注意我在表单元素上添加了自定义className属性,以便我们可以从外部组件动态设置类,否则它将不起作用。我们只想在用户点击“设置账户”时显示这个表单,所以我们将表单组件添加到我们的三个按钮下面作为一个隐藏元素,因为这个位置对用户来说更有意义,因为它更靠近鼠标。我们如何在网站中隐藏一个元素?通过使用一个自定义类,将显示设置为 none。

首先,在构造函数中设置新的状态变量,以在不必要时隐藏表单:

class Main extends React.Component {
    constructor() {
        super()

        this.state = {
            isFormHidden: true
        }
    }
}

然后,在按钮下面添加Form组件,并使用一个动态类名来根据需要显示表单:

render() {
  return (
      <div>
          <h1>Welcome to Decentralized Social Music!</h1>
          <p>Setup your account, start adding musical recommendations for your friends and follow people that may interest you</p>
          <div className="buttons-container">
              <button>Setup Account</button>
              <button>Add Music</button>
              <button>Follow People</button>
          </div>

          <Form className={this.state.isFormHidden ? 'hidden' : ''} />

          <h3>Latest musical recommendations from people using the dApp</h3>
          <div ref="general-recommendations">
              <Recommendation
                  name="John"
                  address="0x5912d3e530201d7B3Ff7e140421F03A7CDB386a3"
                  song="Regulate - Nate Dogg"
              />
              <Recommendation
                  name="Martha"
                  address="0x1034403ad2f8e9da55272CEA16ec1f2cBdae0E5c"
                  song="X - Xzibit"
              />
          </div>
      </div>
  )
}

正如你所看到的,我添加了一个叫做isFormHidden的状态元素,它指示表单是否隐藏。然后,我设置了我们的FormclassName作为一个依赖于状态的动态组件,以便在适当的时候保持隐藏。我们需要使用 React 的状态,因为这是 React 更新正在显示的信息的主要方式。React 对状态是响应式的,所以每当它改变时,它就会更新整个 Web 应用程序。如果我们简单地选择组件并直接更新它的类,React 就不会知道发生了什么,并且会变得混乱,因为状态是每个交互式 Web 应用程序的重要组成部分。

然后,创建一个 CSS 类来隐藏它,如下所示:

.hidden {
    display: none;
}

在实时页面上查看结果。你不应该看到任何东西,因为你的表单是隐藏的。要显示它,你必须在“设置账户”按钮上添加一个onClick事件,如下所示:

<button onClick={() => {
    if(this.state.isFormHidden) this.setState({isFormHidden: false})
    else this.setState({isFormHidden: true})
}}>Setup Account</button>

这将读取表单的状态以在点击时隐藏或显示它。你会发现设计是一团糟,所以我们必须改进它:

为每个输入添加一个新的通用类,用不同的类来区分文本区域,如下所示:

<form className={this.props.className}>
    <input className="form-input" type="text" ref="form-name" placeholder="Your name" />
    <input className="form-input" type="number" ref="form-age" placeholder="Your age" />
    <textarea className="form-input form-textarea" ref="form-state" placeholder="Your state, a description about yourself"></textarea>

    <div>
        <button className="cancel-button">Cancel</button>
        <button>Submit</button>
    </div>
</form>

然后,创建具有所需外观的新 CSS 类,如下所示:

.form-input {
    display: block;
    width: 200px;
    border-radius: 20px;
    padding: 20px;
    border: 1px solid #444444;
    margin: 10px auto;
}

.form-textarea {
    height: 200px;
}

.cancel-button {
    margin-right: 10px;
}

这是在样式更改后的外观:

看起来好多了。

实现设置功能

现在,我们必须让它与我们的智能合约交互。为此,我们将不得不在ropsten上创建我们部署的合约的新实例。我们需要地址和ABI接口,你可以在 Truffle 为你部署SocialMusic合约时创建的build/contracts/文件夹中快速找到它们。只需将SocialMusic.json复制到你的src/文件夹以便更容易访问。请记住,如果决定扩展其功能,则需要用新的 ABI 版本替换该文件。只需重复相同的步骤即可:

接下来,我们需要一种方法来在我们的 React 应用程序中导入该 JSON 文件。幸运的是,如果你使用的是 webpack 2.0 或更新版本(我目前使用的是 4.19),则无需执行任何额外操作,因为 webpack 默认支持 JSON 文件。在之前的版本中,你必须添加一个新的json-loader来处理这些文件。只需在文件开头添加文件,如下所示:

import React from 'react'
import ReactDOM from 'react-dom'
import web3 from 'web3'
import './index.css'
import ABI from './SocialMusic.json'

你可以使用你想要的名称导入你的 JSON 文件;ABI变量的目的是为了能够读取 JSON 文件的值。然后,创建一个变量,其中包含你的智能合约的地址和你的abi接口。请记住,你可以随时使用 Truffle 使用deploy --network ropsten --reset部署新版本以获取新地址(如果你丢失了它):

class Main extends React.Component {
    constructor() {
        super()

        const contractAddress = '0x0217ED41bC271a712f91477c305957Da44f91068'
        const abi = ABI.abi

        this.state = {
            isFormHidden: true
        }
    }
    ...
}

我们希望使用我们自己版本的 web3 1.0 部署合约,因为 MetaMask 注入的版本已过时,我们无法依赖于不受控制的版本。这就是为什么我们会创建一个新的 web3 实例,如下所示:

import React from 'react'
import ReactDOM from 'react-dom'
import myWEB3 from 'web3'
import './index.css'
import ABI from './SocialMusic.json'

class Main extends React.Component {
    constructor() {
        super()

        window.myWeb3 = new myWEB3(myWEB3.givenProvider)
        const contractAddress = '0x0217ED41bC271a712f91477c305957Da44f91068'
        const abi = ABI.abi

        this.state = {
            isFormHidden: true
        }
    }
...

我将web3变量重命名为myWeb3,以避免与 MetaMask 注入的变量混淆。请注意myWeb3前面的window关键字;这用于将该变量设置为全局,以便你可以从你的 dApp 中的任何位置访问myWeb3。通过在任何地方都可以访问我们的自定义 web3,生活会变得更加轻松。稍后我们将使用带有 async await 的 promises。为了能够在这个版本的 webpack/babel 中使用 async await,你需要安装babel-polyfill,它负责编译你的 async 代码,以便在所有浏览器上正常工作。使用以下命令安装:

npm i -S babel-polyfill

然后,将其添加到你的 webpack 配置文件中,如下所示:

require('babel-polyfill')
const html = require('html-webpack-plugin')
const path = require('path')

module.exports = {
    entry: ['babel-polyfill', './src/index.js'],
    output: {
        filename: 'bundle.js',
        path: path.join(__dirname, 'dist')
    },
...

现在,我们将使用一些辅助函数和setupAccount函数的集成来创建合约的实例。

首先,更新构造函数以在 dApp 加载时执行设置合约实例的函数,如下所示:

constructor() {
    super()

    window.myWeb3 = new myWEB3(myWEB3.givenProvider)
    this.state = {
        isFormHidden: true
    }

    this.setContractInstance()
}

然后,创建所需的函数来正确设置用户帐户和合约,如下所示:

async getAccount() {
 return (await myWeb3.eth.getAccounts())[0]
}

async setContractInstance() {
    const contractAddress = '0x0217ED41bC271a712f91477c305957Da44f91068'
    const abi = ABI.abi
    const contractInstance = new myWeb3.eth.Contract(abi, contractAddress, {
        from: await this.getAccount(),
        gasPrice: 2e9
    })
    await this.setState({contractInstance: contractInstance})
}

async setupAccount(name, age, status) {
    await this.state.contractInstance.methods.setup(this.fillBytes32WithSpaces(name), age, status).send({from: '0x2f6ccd575FA71e2912a31b65F7aFF45C8bf91155'})
}

fillBytes32WithSpaces(name) {
    let nameHex = myWeb3.utils.toHex(name)
    for(let i = nameHex.length; i < 66; i++) {
        nameHex = nameHex + '0'
    }
    return nameHex
}

然后,在你的render()函数中使用Form props 来告诉 React 用户单击设置按钮时要执行什么操作,以及用户单击取消按钮时要执行什么操作:

render() {
    return (
        <div>
            <h1>Welcome to Decentralized Social Music!</h1>
            <p>Setup your account, start adding musical recommendations for your friends and follow people that may interest you</p>
            <div className="buttons-container">
                <button onClick={() => {
                    if(this.state.isFormHidden) this.setState({isFormHidden: false})
                    else this.setState({isFormHidden: true})
                }}>Setup Account</button>
                <button>Add Music</button>
                <button>Follow People</button>
            </div>

            <Form
                className={this.state.isFormHidden ? 'hidden' : ''}
                cancel={() => {
                    this.setState({isFormHidden: true})
                }}
                setupAccount={(name, age, status) => {
                    this.setupAccount(name, age, status)
                }}
            />

            <h3>Latest musical recommendations from people using the dApp</h3>
            <div ref="general-recommendations">
                <Recommendation
                    name="John"
                    address="0x5912d3e530201d7B3Ff7e140421F03A7CDB386a3"
                    song="Regulate - Nate Dogg"
                />
                <Recommendation
                    name="Martha"
                    address="0x1034403ad2f8e9da55272CEA16ec1f2cBdae0E5c"
                    song="X - Xzibit"
                />
            </div>
        </div>
    )
}

最后,更新你的Form组件,添加新功能以在用户与输入交互时触发设置功能:

class Form extends React.Component {
    constructor() {
        super()
    }

    render() {
        return (
            <form className={this.props.className}>
                <input className="form-input" type="text" ref="form-name" placeholder="Your name" />
                <input className="form-input" type="number" ref="form-age" placeholder="Your age" />
                <textarea className="form-input form-textarea" ref="form-state" placeholder="Your state, a description about yourself"></textarea>
                <div>
                    <button onClick={event => {
                        event.preventDefault()
                        this.props.cancel()
                    }} className="cancel-button">Cancel</button>
                    <button onClick={event => {
                        event.preventDefault()
                        this.props.setupAccount(this.refs['form-name'].value, this.refs['form-age'].value, this.refs['form-state'].value)
                    }}>Submit</button>
                </div>
            </form>
        )
    }
}

这里有一些变化,让我解释一下我做了什么。

首先,我创建了setContractInstance函数,用于使用我们智能合约的地址设置合约实例,以便以后可以使用它执行其他功能。getAccount函数是一个快速获取当前用户地址的辅助函数。

然后,我创建了setupAccount函数,它接收我们想要用于设置用户帐户的三个参数,并使用一个名为fillBytes32WithSpaces的辅助函数,因为我们需要用这个版本的web3.js填充一个 bytes32 类型变量的所有空格,否则它会拒绝交易。此函数简单地创建了我们部署的智能合约中的setup()函数的事务。

接下来,我为Form组件创建了一些属性函数,当用户点击取消或提交时将执行这些函数。我们希望当用户取消时隐藏表单,所以我简单地将表单的状态设置为隐藏。当用户点击提交时,我们提取所有输入的数据,并将它们发送到setupAccount函数。请注意,我在每个按钮的点击事件内部都使用了event.preventDefault(),以避免刷新页面,因为所有的 HTML 按钮都是提交按钮,它们应该向服务器发送信息。

注意,在设置用户数据时我们使用了.send()函数,这会生成一个交易并花费 gas。在其中,我使用了我的以太坊地址,以便它知道谁应该进行交易:

.send({from: '0x2f6ccd575FA71e2912a31b65F7aFF45C8bf91155'})

但是你不想使用相同的地址,因为你无法访问它来使用你的 MetaMask。你可以简单地删除那个参数,让函数看起来像这样:

.send()

有时告诉 React 自动查找用户的地址不起作用,所以你可以设置自己的地址。记得解锁你的 MetaMask 并使用 ropsten,然后把你当前的地址粘贴到那里。

更新完变化后,与您的 dApp 进行交互,以验证它确实正在向智能合约提交交易。

现在我们要做的是设置“添加音乐”按钮,使用户可以创建音乐推荐。首先,通过更新构造函数内的状态对象,创建一个新的组件设计,就像我们之前做的那样:

constructor() {
    super()

    window.myWeb3 = new myWEB3(myWEB3.givenProvider)
    this.state = {
        isFormHidden: true,
        isAddMusicHidden: true
    }

    this.setContractInstance()
}

然后,创建一个新的addMusic()函数,它将把指定的歌曲推送到数组中:

async addMusic(music) {
    await this.state.contractInstance.methods.addSong(music).send({from: '0x2f6ccd575FA71e2912a31b65F7aFF45C8bf91155'})
}

通过向“添加音乐”按钮添加onClick事件监听器来更新state以显示添加音乐表单。然后,添加新的AddMusic组件,就像这样:

render() {
    return (
        <div>
            <h1>Welcome to Decentralized Social Music!</h1>
            <p>Setup your account, start adding musical recommendations for your friends and follow people that may interest you</p>
            <div className="buttons-container">
                <button onClick={() => {
                    if(this.state.isFormHidden) this.setState({isFormHidden: false})
                    else this.setState({isFormHidden: true})
                }}>Setup Account</button>
                <button onClick={() => {
                    if(this.state.isAddMusicHidden) this.setState({isAddMusicHidden: false})
                    else this.setState({isAddMusicHidden: true})
                }}>Add Music</button>
                <button>Follow People</button>
            </div>

            <Form
                className={this.state.isFormHidden ? 'hidden' : ''}
                cancel={() => {
                    this.setState({isFormHidden: true})
                }}
                setupAccount={(name, age, status) => {
                    this.setupAccount(name, age, status)
                }}
            />

            <AddMusic
                className={this.state.isAddMusicHidden ? 'hidden': ''}
                cancel={() => {
                    this.setState({isAddMusicHidden: true})
                }}
                addMusic={music => {
                    this.addMusic(music)
                }}
            />

            <h3>Latest musical recommendations from people using the dApp</h3>
            <div ref="general-recommendations">
                <Recommendation
                    name="John"
                    address="0x5912d3e530201d7B3Ff7e140421F03A7CDB386a3"
                    song="Regulate - Nate Dogg"
                />
                <Recommendation
                    name="Martha"
                    address="0x1034403ad2f8e9da55272CEA16ec1f2cBdae0E5c"
                    song="X - Xzibit"
                />
            </div>
        </div>
    )
}

最后,使用class函数定义新的AddMusic组件:

class AddMusic extends React.Component {
    constructor() {
        super()
    }

    render() {
        return(
            <div className={this.props.className}>
                <input type="text" ref="add-music-input" className="form-input" placeholder="Your song recommendation"/>
                <div>
                    <button onClick={event => {
                        event.preventDefault()
                        this.props.cancel()
                    }} className="cancel-button">Cancel</button>
                    <button onClick={event => {
                        event.preventDefault()
                        this.props.addMusic(this.refs['add-music-input'].value)
                    }}>Submit</button>
                </div>
            </div>
        )
    }
}

我们遵循了创建Form组件时所做的相同步骤。只需设置渲染 HTML,将AddMusic元素放在Form元素下方,同时保持隐藏,并设置所有 prop 函数。然后,创建一个将新歌曲添加到智能合约的函数。我们还为切换这些按钮的隐藏类创建了一个新的状态变量。

您可能已经注意到,如果您先单击“添加歌曲”,然后立即单击“设置帐户”而不取消,那些 div 会保持打开状态——这不是我们想要的。我们希望在任何给定时间内仅保持一个部分处于打开状态。我们可以通过一个函数来更新隐藏所有组件的状态来实现这一点,然后再打开新的组件,如下所示:

hideAllSections() {
    this.setState({
        isFormHidden: true,
        isAddMusicHidden: true
    })
}

然后,在打开部分之前添加函数调用到按钮:

<button onClick={() => {
    this.hideAllSections()
    if(this.state.isFormHidden) this.setState({isFormHidden: false})
    else this.setState({isFormHidden: true})
}}>Setup Account</button>
<button onClick={() => {
    this.hideAllSections()
    if(this.state.isAddMusicHidden) this.setState({isAddMusicHidden: false})
    else this.setState({isAddMusicHidden: true})
}}>Add Music</button>

让我们添加最终的按钮功能,以便关注其他用户。我们将显示所有已注册用户的列表,以便用户可以关注自己喜欢的用户以获取更新。为了实现这一点,我们将不得不修改我们的合同,以便我们可以添加一个包含最新新人的数组,当用户执行setup函数时会更新该数组,如下所示:

pragma solidity 0.5.0;

contract SocialMusic {
    struct User {
        bytes32 name;
        uint256 age;
        string state; // A short description of who they are or how they feel
        string[] musicRecommendations;
        address[] following;
    }
    mapping(address => User) public users;
    address[] public userList;

    // To add a new musical recommendation
    function addSong(string memory _songName) public {
        require(bytes(_songName).length > 0 && bytes(_songName).length <= 100);
        users[msg.sender].musicRecommendations.push(_songName);
    }

    // To setup user information
    function setup(bytes32 _name, uint256 _age, string memory _state) public {
        require(_name.length > 0);
        User memory newUser = User(_name, _age, _state, users[msg.sender].musicRecommendations, users[msg.sender].following);
        users[msg.sender] = newUser;
        userList.push(msg.sender);
    }

    // To follow new users
    function follow(address _user) public {
        require(_user != address(0));
        users[msg.sender].following.push(_user);
    }

    // Returns the array of users
    function getUsersList() public view returns(address[] memory) {
        return userList;
    }
}

使用 Truffle 重新部署合同,如下所示:

truffle deploy --network ropsten --reset

我们将直接从构建文件夹中获取所有信息,包括地址,而不是手动复制新地址并更新 json 文件,如下所示:

import ABI from '../build/contracts/SocialMusic.json'

...
async setContractInstance() {
    const contractAddress = ABI.networks['3'].address
    const abi = ABI.abi
    const contractInstance = new myWeb3.eth.Contract(abi, contractAddress, {
        from: await this.getAccount(),
        gasPrice: 2e9
    })
    await this.setState({contractInstance: contractInstance})
}
...

现在,您不必每次都担心更新信息,这很棒,因为您可以自由访问不同的外部文件夹,webpack 会获取并捆绑所有所需信息,因此访问src/文件夹之外的文件并不重要。现在让我们创建所需的功能,以获取最新的用户,以便用户可以关注他们,并添加一些新组件:

class FollowPeopleContainer extends React.Component {
    constructor() {
        super()
    }

    render() {
        let followData = this.props.followUsersData
        // Remove the users that you already follow so that you don't see em
        for(let i = 0; i < followData.length; i++) {
            let indexOfFollowing = followData[i].following.indexOf(this.props.userAddress)
            if(indexOfFollowing != -1) {
                followData = followData.splice(indexOfFollowing, 1)
            }
        }
        return (
            <div className={this.props.className}>
                {followData.map(user => (
                    <FollowPeopleUnit
                        key={user.address}
                        address={user.addres}
                        age={user.age}
                        name={user.name}
                        state={user.state}
                        recommendations={user.recommendations}
                        following={user.following}
                        followUser={() => {
                            this.props.followUser(user.address)
                        }}
                    />
                ))}
            </div>
        )
    }
}

class FollowPeopleUnit extends React.Component {
    constructor() {
        super()
    }

    render() {
        return (
            <div className="follow-people-unit">
                <div className="follow-people-address">{this.props.address}</div>
                <div className="follow-people-name">{myWeb3.utils.toUtf8(this.props.name)}</div>
                <div className="follow-people-age">{this.props.age}</div>
                <div className="follow-people-state">"{this.props.state}"</div>
                <div className="follow-people-recommendation-container">
                    {this.props.recommendations.map((message, index) => (
                        <div key={index} className="follow-people-recommendation">{message}</div>
                    ))}
                </div>
                <button
                    className="follow-button"
                    onClick={() => {
                        this.props.followUser()
                    }}
                >Follow</button>
            </div>
        )
    }
}

FollowPeopleContainer只是一个组件,它保存了您可以用您的账户关注的所有用户。它将从Main组件中的this.props.followUsersData prop 接收数据,该数据发送一个包含最多 10 位用户和每位用户最多两个音乐推荐的数组,以便您可以了解他们的类型。它还从数组中删除已经关注的用户,以便您不将其视为新用户。最后,它使用一个包含所有所需用户属性的FollowPeopleUnit组件,并具有一个将传输所需关注特定用户信息到Main组件的函数。

注意在每个FollowPeopleUnit中的key={user.address}属性,因为我们需要能够单独识别它们,这是 React 强制性需要的,以避免重复元素。

另一方面,FollowPeopleUnit组件由一组显示所有必要信息给用户的 div 组成。因为我们在一个名为this.props.recommendations的数组中有两个推荐,所以我们必须循环遍历它们以逐个显示每个消息。当你想要使用 react 动态生成 HTML 元素时,你必须使用你的数组的.map()函数,并用圆括号()而不是花括号{},因为所有 HTML 元素必须在这些类型的括号内。

现在我们有了两个新组件,我们必须在我们的Main组件中定义函数以使它们交互式;你可以在 GitHub 上查看完整的代码,这样你就知道所有东西应该放在哪里,以防你遇到错误。

首先,我们更新了地址系统,使其动态化,这样你就不必在代码中手动输入你的以太坊地址。例如,假设我们有以下代码:

await this.state.contractInstance.methods.follow(address).send({from: '0x2f6ccd575FA71e2912a31b65F7aFF45C8bf91155'})

相反,我们将使用以下代码,这对于使我们的 dApp 与许多不同的用户交互是必不可少的。您可以看到我们在setContractInstance()函数中设置了userAddress状态变量:

await this.state.contractInstance.methods.follow(address).send({from: this.state.userAddress})

接下来,我们创建了复杂的getFollowPeopleUsersData()函数,该函数获取最近的用户地址;它最多获取 10 个用户地址,如果用户数量不够,则获取尽可能多的用户。然后,它创建一个包含我们想要的所有属性的userData对象,并使用智能合约状态变量的信息填充它,首先通过使用getUsersMusicRecommendationLength()获取音乐推荐数组的长度,然后通过使用getUsersMusicRecommendation()获取每个单独的音乐推荐。在函数的底部,我们获取了那个特定人正在关注的用户数组,以防我们需要访问它们。

正如你所看到的,我们使用了智能合约中的一些新函数。这是因为如果不添加一些复杂性,我们无法使所有这些成为可能。这是我们更新后的智能合约的样子:

pragma solidity 0.5.0;

contract SocialMusic {
    struct User {
        bytes32 name;
        uint256 age;
        string state; // A short description of who they are or how they feel
        string[] musicRecommendations;
        address[] following;
    }
    mapping(address => User) public users;
    address[] public userList;

    // To add a new musical recommendation
    function addSong(string memory _songName) public {
        require(bytes(_songName).length > 0 && bytes(_songName).length <= 100);
        users[msg.sender].musicRecommendations.push(_songName);
    }

    // To setup user information
    function setup(bytes32 _name, uint256 _age, string memory _state) public {
        require(_name.length > 0);
        User memory newUser = User(_name, _age, _state, users[msg.sender].musicRecommendations, users[msg.sender].following);
        users[msg.sender] = newUser;
        userList.push(msg.sender);
    }

    // To follow new users
    function follow(address _user) public {
        require(_user != address(0));
        users[msg.sender].following.push(_user);
    }

    // Returns the array of users
    function getUsersList() public view returns(address[] memory) {
        return userList;
    }

    // Returns the music recommendations
    function getUsersMusicRecommendation(address _user, uint256 _recommendationIndex) public view returns(string memory) {
        return users[_user].musicRecommendations[_recommendationIndex];
    }

    // Returns how many music recommendations that user has
    function getUsersMusicRecommendationLength(address _user) public view returns(uint256) {
        return users[_user].musicRecommendations.length;
    }

    // Returns the addresses of the users _user is following
    function getUsersFollowings(address _user) public view returns(address[] memory) {
        return users[_user].following;
    }
}

有一些函数用于从各自的数组中检索每个特定人的以下和推荐数据。这是因为我们不能只使用公共数组自动获取整个数组;正如你所知,公共数组每次只返回整个数组的一个元素,所以我们需要一个函数来完整获取它。字符串也是同样的情况——我们必须创建一个函数来逐个获取每个字符串,因为我们不能只发送一个字符串数组,考虑到它们是低级多维度的byte[][]数组。Solidity 不允许你发送byte[][][],它将是等同于string[]的等价物,因为它太大且太复杂了。

记得在用 Truffle 进行更改后重新部署你的代码,deploy --network ropsten --reset。现在,你不需要更新智能合约地址,也不需要将 ABI 复制到源文件夹中,因为它被设置为直接从构建文件夹中获取部署的合约数据。

我们的 dApp 看起来还不够好,所以这里是你参考的整个 CSS 代码,如果你想要达到相同的外观:

body {
    margin: 0;
    font-family: sans-serif;
    text-align: center;
}

button {
    border-radius: 10px;
    padding: 20px;
    color: white;
    border: none;
    background-color: rgb(69, 115, 233);
    cursor: pointer;
}

button:hover {
    opacity: 0.7;
}

.hidden {
    display: none;
}

.buttons-container button:not(:last-child){
    margin-right: 5px;
}

.recommendation {
    width: 40%;
    margin: auto;
    background-color: whitesmoke;
    border-radius: 20px;
    margin-bottom: 10px;
    padding: 40px;
}

.follow-people-unit {
    width: 40%;
    background-color: whitesmoke;
    border-radius: 20px;
    margin: 10px auto;
    padding: 20px;
}

.recommendation-name, .recommendation-address, .follow-people-address, .follow-people-name, .follow-people-age {
    display: inline-block;
    color: #444444;
    font-style: italic;
}

.recommendation-name, .follow-people-name {
    margin-right: 10px;
}

.recommendation-address, .follow-people-address {
    color: rgb(156, 156, 156);
}

.recommendation-song, .follow-people-recommendation {
    font-weight: bolder;
    font-size: 16pt;
    margin-top: 10px;
}

.form-input {
    display: block;
    width: 200px;
    border-radius: 20px;
    padding: 20px;
    border: 1px solid #444444;
    margin: 10px auto;
}

.form-textarea {
    height: 200px;
}

.cancel-button {
    margin-right: 10px;
}

.follow-people-state {
    font-style: italic;
    font-weight: bolder;
    color: #444444;
}

.follow-button {
    margin-top: 10px;
}

这是最终结果的截图,你可以通过webpack -dhttp-server dist/tolocalhost:8080上看到它:

继续点击前面截图中显示的按钮与你的新的很棒的 dApp 进行交互吧。关注人们按钮将需要几秒钟的时间从智能合约中加载数据,因为我们正在运行几个请求来生成我们在 JavaScript 中的自定义对象。

最后,我可以添加功能来显示最新的音乐推荐和取消关注系统,但我将把这留给你作为一个练习,来提高你的智能合约 dApp 实现技能。这个想法是在页面底部动态显示生成的推荐组件,而不是我们已经有的静态组件;你可以通过简单地从我们的智能合约中获取数据来实现这一点。

这个 dApp 远非完美;你可以通过优化数据结构来解决一些速度问题,这样你就只检索所需的信息而不是整个数组。你也可以通过用 truffle 测试代码来解决一些安全问题。最终结果取决于你;你决定何时可以认为应用程序已经完成,因为你可以添加并继续添加使其更好的功能——这就是所有伟大的 dApp 的诞生方式。

你可以在我的 GitHub 上查看最终代码:github.com/merlox/social-music

摘要

在本章中,你亲自看到了如何创建一个去中心化应用程序,以及所需的所有细微差别和更改。你已经经历了整个过程,从使用 React、webpack 和 Truffle 设置开发环境开始。你学会了如何创建整洁地组织代码的 React 组件,以便你可以轻松地管理你的 dApp 的所有复杂性。

记得把这个应用程序加到你的 GitHub 上作为证明你已经完成了它,以提高你的简历上有价值的项目,这样未来的客户就可以亲眼看到你为他们做了什么,以及你已经掌握了构建完全去中心化应用程序所需的所有步骤。在下一章中,你将学到更多关于如何通过进一步的高级技巧来改进你的 dApps,使它们感觉灵敏,并表现得像高质量的系统一样。

第六章:工具、框架、组件和服务

在本章中,您将了解到关于以太坊开发者可用的几个重要工具,用于创建强大的分布式应用和可为数百万潜在用户提供安全服务的智能合约。以太坊开发世界充满了许多有用的工具,旨在让您在创建复杂的分布式应用和智能合约时更轻松,这些应用和智能合约使用了 Solidity 的最新变化。了解存在的工具和它们如何运作将极大地帮助您推进您的开发项目,因为您将能够创建更好,更快速的应用程序,减少易出错的代码。

在本章中,我们将涵盖以下主题:

  • 使用开发者工具

  • 了解以太坊通信工具

  • 理解以太坊基础设施

  • 学习以太坊测试和安全工具

  • 获取重要的开源库

使用开发者工具

开发者工具,例如集成开发环境IDEs)、水龙头,甚至智能合约语言是开发人员必须掌握的基本事项,以便在开发智能合约时能够实际理解如何高效地开发。

开发框架

有几个开发框架为您提供了一组工具,用于在可以测试和验证代码的环境中创建智能合约,从而以较高质量的代码提高开发速度。让我们看看一些最受欢迎的框架,以决定在任何特定时刻应该使用哪一个:

  • Truffle:这是用 Solidity 创建 dApps 和智能合约的最大开发框架。在撰写本文时,它与 Vyper 并不完全兼容,但如果你真的希望如此,有一些解决方法可以让它正常运行。正如你已经知道的,Truffle 可以直接在终端中为您提供智能合约编译、部署和测试工具,这样您就无需离开您的工作流程。它的设置有点复杂,因为你必须按照特定的方式工作,但一旦完成,你将拥有创造强大 dApp 的巨大潜力。你可以在truffleframework.com获取它。

  • Waffle:虽然不是一个知名的开发框架,但 Waffle 旨在创建更简单和更快的编程工具,以便您可以在有更少的依赖性的情况下轻松开发。你只需要 contractstest 文件夹来开始使用 Waffle,因为它可以在没有复杂性的情况下编译所有代码。你可以使用 npx waffle 轻松编译你的智能合约。由于它的目标是尽可能精简,你无法从它们的工具中部署合约,也没有像 Truffle 那样的 build/ 文件夹,所以你需要自行部署它们。少一些设置和头痛,功能更少但更简单。你可以用 npm i -S Ethereum-waffle 来安装它。

  • 0xcert: 这是另一个优秀的开发工具,专注于创建和部署高级 ERC721 非同质化智能合约代币。这些是特殊类型的代币,其中每个代币都是唯一的,并具有某种内在价值。例如,CryptoKitties 使用 ERC721 代币生成具有唯一特征和基于稀有度的固定价格的随机动物。Oxcert 旨在增加已被接受的 ERC721 代币标准的采用率,以便开发人员可以创建更快、更安全和更复杂的代币合约。您可以在他们的网站上获取它:0xcert.org

集成开发环境

当谈到集成开发环境(IDEs)时,我们有一小部分工具真正帮助您从第一行代码起编写安全的程序,因为它们试图在错误发生之前修复错误:

  • Remix: 最受欢迎的开发环境是一个非常强大的代码编辑器,可以通过自动编译和有用的警告消息来修复您的智能合约代码,以指示您的代码存在什么问题。它甚至提供最佳实践的建议,让您在开发过程中学习。您可以使用自定义以太坊实例、JavaScript 虚拟机或注入的web3.js来部署您的智能合约,以查看您的智能合约在不同环境中的反应。一旦您有源代码,您就可以在不离开浏览器的情况下与每个部署的合约进行交互。我强烈推荐它用于开发 Solidity 代码并手动测试函数的每个组件。您可以在remix.ethereum.org上开始使用它。

  • Superblocks: 这是一个强大的 IDE,甚至包含更多功能,可以直接部署您的 dApps,这样您就可以与您的应用程序进行实时交互并获得即时反馈。您可以导出您的 dApps,部署合约,直接访问区块链等等很多很棒的功能。您应该给它一个机会,体验一次从空文件到功能齐全且经过测试的 dApp 的每一步都得到帮助的完整 IDE 的力量。

测试网水龙头

作为以太坊开发人员,您可能已经熟悉可供您使用的测试网络。您知道它们是强大的区块链,可以在其中部署您的智能合约,这是一个安全的空间,甚至可以用于真实世界的应用程序,因为功能是相同的。让我们看看以下一些水龙头,这样您就可以体验每个测试区块链的不同特性:

  • Rinkeby:这是一个权威证明PoA)区块链,你可以通过质押机制挖掘交易而不泄露你的身份。这是一个非常强大的解决方案,用于安全可靠的区块链。你应该在不同的测试网络上部署你的应用程序,以找到最适合你要求的测试网络。如果你想使用 Rinkeby,你需要一些测试以太币,在 rinkeby.io/#faucet 可以获得。这个过程与其他区块链有些不同,因为他们想确保网络保持不受损害。所以,为了获得以太币,你必须在社交媒体平台上发布你的以太坊地址,如 Twitter,并等待接收一定数量的以太币:一次最多可获得 18.75 个以太币,每 3 天可以提取一次。

在发布你的以太坊地址后,将链接粘贴到水龙头上,你将在几分钟内收到你的以太币。这个网络的好处是与 MetaMask 兼容,而且区块时间非常快。

  • Ropsten:这是最流行的工作量证明PoW)测试网络,在这里你可以从许多水龙头快速获取以太币。你可以从 MetaMask 自身获得免费以太币,只需访问 faucet.metamask.io 就可以收到有限数量的以太币。每个用户的以太币限制不断变化,所以很难预测通过反复点击请求来自水龙头的 1 个以太币按钮你将获得多少以太币——这取决于你自己去发现。尽管如此,这是一个很棒的区块链,虽然由于其低容量区块链,只有少量节点免费挖矿,因此不像其他区块链那样可靠。

  • Kovan:尽管这不太常用于测试项目,但它是一个非常稳固的 PoA 测试网络,由 Parity 团队构建,为开发者提供了一个高效的测试环境,适用于各种项目。你可以在 faucet.kovan.network 获得以太币,在那里你需要用你的 GitHub 账户登录,每个账户每 24 小时可以收到 1 个以太币。虽然不多,但对于没有实质性支付要求的较小项目来说应该足够了。

了解以太坊通信工具

以太坊是一个由几个相互连接的部分组成的大生态系统,这些部分相互交流,包括智能合约、dApps 和 Web 应用程序。目标是创建单独的结构,使你的最终应用程序是模块化的,这样你就可以更新特定部分而不必重新创建整个 dApp。这就是为什么我们有通信工具:帮助我们在智能合约、dApps 和 Web 应用程序之间交换信息的库。让我们来看看在通信方面我们现在有哪些不同的工具。

以太坊前端 API

谈到前端通信工具,我们有一些强大的 JavaScript 库,通过连接智能合约和 Web 应用程序,使 dApps 成为可能:

  • Web3.js:最流行的用于在 web 应用程序中使用智能合约的库,可以创建合约实例、调用合约函数、创建交易和签名交易。单单这个库就使得 dApps 成为可能。后端是区块链本身的 Web 应用程序是一个革命性的概念,因为人们决定构建了这样的库,它正因此而日益受到欢迎。它也可以在 Node.js 应用程序中使用,因此对于包括后端在内的各种 dApps 都是一个很好的工具。你可以在这里获取 web3.js:github.com/Ethereum/web3.js

  • NEthereum:这是一款类似于 web3.js 的智能合约通讯工具,专为.NET 开发人员而设计。那些使用流行的.NET 库并喜欢使用 C#编程的人会喜欢这个工具,因为它专为这些程序员而制作。它为你提供了连接现有.NET 环境与你的 Web 应用程序所需的一切,包括库和客户端集成。你可以在这里了解更多关于 NEthereum 的信息:nethereum.com

  • Drizzle:这是一个为你的 dApps 提供 Redux 集成的工具,可以轻松管理状态和数据存储。如果你熟悉 Redux 和 React,你会喜欢这个工具,因为它为你提供了一个干净的库,可以轻松实现 Redux 在你的 dApps 上的好处。由 Truffle 的创建者制作,非常适合大型项目。你可以在这里获取它:github.com/trufflesuite/drizzle

以太坊后端 API

大多数以太坊应用程序都需要某种形式的中心化后端来执行一些对智能合约来说不可行的任务,要么是因为超出了合同的能力,要么是因为有更好的处理某些操作的方法。在这些情况下,我们仍然需要与区块链进行通信。这就是后端 API 出场的地方:帮助我们创建工具和系统,改善我们总体的 dApps 和智能合约。

  • Web3.py:这是用于 Python 的流行的以太坊实现,可以为这种流行的语言创建工具和系统。Python 与 web3.js 非常搭配,因为你可以创建高效的脚本来自动执行一些操作,比如检查智能合约事件。其语法与原始的web3.js非常相似,因此你会感到很舒适使用它。在这里查看它:github.com/Ethereum/web3.py

  • Eventeum:这是一个用于与后端微服务通信智能合约事件的工具。如果您对微服务不熟悉,它们只是专注于以非常高效和可维护的方式执行某些特定任务的小型应用程序,以便最终应用程序非常高效,并且易于使用模块进行优化和替换。Eventeum 正在与这些微服务一起工作,以处理您智能合约生成的以太坊事件,以便您可以轻松地在复杂的服务网络上实施事件。它适用于 Java,非常适合希望实现可伸缩性的后端开发人员。在这里查看它:github.com/ConsenSys/eventeum

应用二进制接口工具

应用二进制接口ABIs)是描述智能合约函数、变量、修饰符以及所有其他内容的 JSON 对象。ABIs 的目标是帮助您的 dApp 快速理解智能合约,以便知道哪些功能对您可用。现在,重要的是尽可能多地利用这一协议,因为您将在所有 dApp 中使用它。以下是一些工具,可以帮助您真正提高对 ABI 的理解:

  • Abi-decoder:这是一个小巧的 JavaScript 工具,允许您解码通常加密并难以理解的复杂交易对象。您知道,每当您通过执行智能合约中的函数发送交易时,您都在与加密数据的区块链进行交互。迟早,您都需要阅读这些交易,无论是因为您正在调试您的 dApp,还是因为您需要出于其他原因了解其中发生了什么。使用 abi-decoder,您可以解码交易数据和交易日志,这对数据高效的 dApp 来说太棒了。在 github.com/ConsenSys/abi-decoder 了解更多关于这个由 Consensys 制作的小工具。

  • OneClickdApp.com:这是一个很棒的工具,可以让您快速将您的 dApp 部署到互联网,而无需担心托管问题。您只需点击一个按钮,选择您的 ABI 数据和配置,您的 dApp 就可以部署到他们的域名上。只需点击几下,您就可以看到它在真实世界中的样子。这对测试和较小的项目非常有用。唯一的问题是,如果决定进行无停机的托管,则您需要每月支付 5 美元,尽管这对您的整体测试过程非常有帮助。在 oneclickdapp.com 了解更多。

到目前为止,您已经发现了一些奇妙的工具,可以立即在您的项目中实施,带来即时的好处。请继续探索完整的技术生态系统,帮助您创建更好的 dApp 和智能合约,以便在未来构建功能强大的应用程序时迈向更高一级。

理解以太坊基础设施

当涉及到以太坊区块链的基本结构时,有几个应用程序可以帮助改善它,以便人们,包括开发人员,能够从其潜力中受益。你知道,区块链能做的远不止处理交易和运行智能合约。你可以通过消息直接与每个节点进行通信,存储信息,并使用自定义客户端。在本节中,您将了解以太坊基础设施中一些最有趣的用例。

以太坊客户端

您已经知道有一些功能强大的客户端,但您可能不知道的是,有专门为特定客户端制作的特定工具。我们将看到一些最好的 Java 编写的实现,主要是因为它是这些类型应用程序中最常使用的语言之一:

  • Pantheon:这款应用完全由 Java 编写,专注于为您的 dApps 和智能合约提供不同的环境。它拥有丰富的文档网站,可以立即开始使用,可以使用 Clique 创建具有 PoW 或 PoA 的私人网络。你不需要了解 Java 就能使用它,因为它非常容易设置。请访问 docs.pantheon.pegasys.tech/en/latest/ 了解更多信息。

  • EthereumJ:这是一个重型实现,专注于为您的私人网络需求提供尽可能多的功能。它可在您的 Maven 或 Gradle 项目中通过 Java 代码进行配置。就易用性而言,这款产品更难设置,并且需要更长的时间来适应它,因为它针对的是企业级开发人员。请访问 github.com/Ethereum/Ethereumj 了解更多信息。

  • Harmony:由 EtherCamp 制作,这是以太坊早期的一个热门网站,提供了他们自己的 IDE 和工具。Harmony 是用 JavaScript 和 Java 的组合编写的,并基于 EthereumJ。他们的目标是提供一个清晰的 Web 仪表板界面,让你可以轻松监控和详细分析区块链。它非常适用于需要深入了解底层运行情况的项目。请访问 github.com/ether-camp/Ethereum-harmony 了解更多信息。

以太坊存储

当我们谈及存储时,指的是在分散的云中保存各种文件,这些云可能与以太坊区块链合作,也可能没有。这些是允许您存储合同和文件的应用程序,而无需依赖于集中式服务器:

  • 星际文件系统IPFS):这是分散式存储的最知名实现,它允许您将大型文件存储在连接的节点的分散网络中,而不是将您的信息存储在集中式服务器和数据库中。它正在被用于各种项目中,这些项目希望利用完全分散的应用程序的可能性,在这些应用程序中,没有集中的故障点。这些类型的项目将来会被广泛使用,因为它们在数千个节点在高质量网络上复制信息时要安全得多。IPFS 介于 Torrent 和 Git 协议之间,其中文件根据其内容进行标识。因此,具有相同内容的两个相同文件将具有相同的加密标识符,称为哈希。这是一个非常重要的革命,因为它们删除了重复的文件,增加了数据的可用性,并允许更好地利用资源,因为许多节点将共享相同的信息,而不是分开的。在他们的网站ipfs.io上查看它。

  • Swarm:这是建立在以太坊之上的一个协议,其目标是以分散的方式共享文件,就像 IPFS 一样,但无需依赖外部团队。它由核心以太坊团队不断改进,并与整个系统无缝集成,因此您可以将其与您的 dApps 和智能合约集成,而无需头痛。在他们的网站swarm-gateways.net上查看它。

以太坊消息传递

以太坊消息传递意味着在对等方之间直接交换加密信息,而无需中介,以便您几乎实时地获得信息。它们是速度优先的聊天和应用程序的绝佳工具:

  • Whisper:最知名的实现对等消息传递的协议,它建立在以太坊之上,并与其核心的所有系统完全集成。您可以使用它与其他 dApps 进行通信,配置最小。智能合约是相互连接的,有一个层次,它们可以安全地共享信息。在这里了解更多关于 Whisper 的信息:github.com/Ethereum/wiki/wiki/Whisper

  • Devp2p:这是建立在以太坊之上的另一个协议,允许用户和 dApps 在不必创建缓慢的区块链交易的情况下以高速交换加密消息。有一个用 Python 编写的实现,称为 pydevp2p,它为您提供了一个简单的界面,以便在您的应用程序中包含消息传递,以便人们可以开始相互交换数据。在他们的官方 GitHub 页面上了解更多信息:github.com/Ethereum/devp2p

以太坊消息应用程序为我们提供了丰富的可能性集,可以创建更好的 dApps 和先进的智能合约,所以当你想要在以太坊上开发基于用户的游戏或聊天应用程序时,你应该关注这些服务。

学习以太坊测试和保护工具

区块链安全是任何其他特性之上的首要任务。没有安全的应用程序,我们无法着手处理最简单的智能合约,因为用户不会信任我们的代码。这就是为什么作为一名精通以太坊开发者,你必须了解如何确保程序安全的所有可能性。

理解监控工具

监控是观察你的应用程序在现实世界中的行为的行为。你知道,监视你的代码非常重要,因为它可能被全世界成千上万的用户使用:你不希望他们在随机时刻有糟糕的体验。确保查看这些工具,以提升你的智能合约水平,因为它们可以极大地提高你的应用程序质量:

  • 智能合约监视器: 由 Neufund 制作,这是一家致力于创建法律约束力智能合约等其他事物的公司。该工具允许你监视你的智能合约活动,并查看它可能导致问题的地方。你可以将其用作自己的定制区块浏览器,作为当你的应用程序中的资金严重减少时的安全工具,或者用于任何需要仔细监视的情况。它可以从终端简单使用,并具有简单的输出界面,以查看发生了什么。在此处了解更多信息:github.com/Neufund/smart-contract-watch

  • Scout: 实时显示智能合约内部事件和活动的发生情况,以便你关注重要的事情。你可以创建在危险情况下应执行的关键事件,以通知你需要紧急修补的安全漏洞。想象一下,如果人们使用像 Scout 这样的工具在遭受黑客攻击时迅速而果断地采取行动,将能够节省多少以太币。他们的仪表板和实时报告令人惊叹,因此我强烈推荐你查看它以改进你的应用程序。在此处了解更多信息:scout.cool

  • Chainlyt: 允许你解码交易数据内部发生的事情,以极端详细的方式探索任何给定时刻发生的情况。你可以将其与其他监控工具结合使用,在太迟之前通过了解事情的发生方式来修补漏洞,因为你可以准确地看到智能合约的内外情况。他们还提供了一个不错的仪表板,你可以自由使用来进行快速项目。这是一个非常强大的工具,适用于高级用户。在此处了解更多信息:chainlyt.io

使用安全测试工具

如果你打算将智能合约部署到主网,并且不想从一开始就面临重要问题,那么测试你的智能合约绝对是必不可少的。这是不可避免的,你应该在开发过程中进行测试。看看这些测试工具,建立一个舒适易用的测试环境,满足你的日常需求:

  • Oyente: 这是一个非常著名的工具,用于轻松分析你的智能合约。他们为你提供了一个基于 Remix 的在线 IDE,具有诸如超时、深度限制、自定义字节码等多种高级功能,以帮助你分析你的智能合约,显著提高其安全性。由于它的潜力,它是非常推荐的。在这里了解更多关于它的信息:oyente.melonport.com

  • MythX: 这是一个奇妙的工具,以清晰的格式显示出在部署之前必须修补的 EVM 字节码问题。这些是低级调用,显示出潜在的安全漏洞。你可以轻松分析它们,甚至已经为 Truffle 和一些开发工具如 Visual Studio Code 提供了插件。它们的主要卖点是为整个安全设置提供的便利,以便你可以使用最常用的工具设置并忘记它们。了解更多关于 Mythx 的信息,请访问:mythx.io

  • Solgraph: 这些是生成的视觉图表,清晰地描述了你的智能合约的流程。例如,如果你想看看当你调用transferFunds()函数时会发生什么,你可以调用 Solgraph,你将收到一个极具直观描述的描述,描述你的合约完成调用所采取的步骤。对于希望了解复杂合约流程的开发者来说,这是非常有效的。在他们的 GitHub 页面上了解更多信息:github.com/raineorshine/solgraph

理解审计工具

审计是手动查看代码各个不同部分的过程,以使用诸如逐行分析、漏洞测试和黑客路径等流程找出潜在的漏洞。你必须熟悉它们以保证可持续、高质量的代码项目。

请注意,它们的目标是加快你的审计流程,因此它们更像是一套深思熟虑的过程的辅助工具:

  • EthSum: 这是一个简单直接的工具,由 Netlify 制作,允许你对以太坊地址进行校验和。有时,你需要对地址进行校验和,以确保它是一个经过正确创建的良好形式的地址。它主要用于 Truffle 项目,其中你必须为你的项目拥有有效的地址,所以 EthSum 是一个不错的辅助工具来验证地址。你可以在这里获取它:ethsum.netlify.com

  • Decode:这是一个使交易数据易于为您的 testrpc 节点理解的工具。当您审核项目时,您必须运行几个测试和手动检查以验证结果的完整性,而大多数情况下很难在 testrpc 或类似的测试环境中执行此操作,因为生成的数据令人困惑。Decode 通过使交易易于阅读和理解来解决了这个问题。了解更多信息,请访问:github.com/dteiml/decode

  • EthToolBox:这是一个具有许多不同实用工具的 Web 应用程序,可帮助您解决常见任务而无需在不同环境之间来回切换。通过绿色界面,您几乎可以进行任何您所需的检查,而无需退出浏览器。它可以执行诸如 ECRecovers、密钥生成、EVM 单词转换、十六进制解析等任务。当您必须审核任何类型的智能合约时,您会喜欢它,因为您可以快速分析任何类型的结果。请在此处从浏览器中使用:eth-toolbox.com

审计工具将为您节省无数时间,避免混乱的错误、漏洞和脆弱性。它们将成为您最常使用的一套工具之一,与您已经出色的开发工作流程结合在一起,以便您一旦集成它们就能创建更好的应用程序。

获取重要的开源库

在创建新的智能合约应用程序时,您必须利用所有可用资源以最有效的方式节省时间或成本来创建它们。您的目标应始终是使用和创建高质量的代码。那么,为什么不在下一个项目中使用一些最常用、经过测试和安全的库呢?它们已被成千上万次地使用,由于其质量而依然强大。让我们在以下章节中看看这些强大的开源库。

ZeppelinOS

Zeppelin 在以太坊领域已经有很长时间了。他们构建了一些最有用的智能合约,比如用于防止溢出问题的 SafeMath,并且有一个充满了安全合同的 GitHub,几乎可以即插即用。他们的智能合约分布在许多文件夹中。为了理解所有这些压缩信息,我们将逐个浏览这些文件夹,为您节省数小时混乱并帮助您了解这些合同的潜力。您可以在官方 GitHub 仓库中访问它们:github.com/openzeppelin/openzeppelin-solidity,看起来像这样:

为了理解所有这些文件以及它们为何重要,我们将解释每个文件夹,以便您快速了解其内部包含的内容。

  • 权限:在这个文件夹中,您将找到提供给特定以太坊用户权限的角色管理合同的工具,以便您可以创建不同角色可以使用的应用程序。

  • 众售:这个文件夹包含一些最有趣的 ICO 智能合同,包括可暂停的、可退款的、可铸造的和白名单众售等各种实现。如果你刚开始学习 ICO,这个文件夹是必不可少的,以了解 ICO 应该如何正确地构建。

  • 加密学:这个文件夹包含了两个用于 Merkle 证明验证和椭圆曲线签名ECDSA)操作的智能合同。这是加密项目中的高级实用工具,用于需要使用已签名消息的加密项目。

  • 草案:这些是正在进行中的智能合同,在完全测试和打磨后将在未来的版本中包含。

  • 示例:这提供了一些快速的代币和 ICO 示例合同,这些合同将所有必要的逻辑实现在单个文件中,以便您可以直观地看到完整的系统运行。

  • 内省:这些是用于检测外部合同中使用的接口的 ERC165 合同。例如,您可以用它来检测特定智能合同中是否支持 ERC20 代币。

  • 生命周期:这个文件夹中包含一个可暂停的智能合同实现,您可以用它来停止任何你希望在任何时间增加安全措施的合同。

  • 数学:可能是最受欢迎的文件夹,其中包含了著名的 SafeMath 库和一个数学智能合同,用于在智能合同中进行安全的数学计算。这些计算是必要的,因为智能合同本质上是不安全的,变量的工作方式使其如此。

  • 模拟:这个文件夹包含许多模拟合同,实现整个合同功能的一小部分,以帮助您理解每种类型合同的关键方面。我建议您从这里开始,了解一个合同与另一个合同的不同之处,比如 ERC 实现。它们实现了可以用于分析这些函数的输入/输出的事件。

  • 所有权:它包含了两个限制函数访问权限的所有者限制合同,其中某些函数必须限制只能被所有者调用。

  • 支付:强大的支付工具,用于组合支付、延迟支付和托管合同,您可以轻松实现。对于依赖常规支付的项目,比如银行,这非常酷。

  • 代币:这个文件夹包含了 ERC20 和 ERC721 的实现,具有许多接口,您可以用它们创建更小或更改进的代币。

  • 实用工具:这个文件夹包括智能合同实用工具,如递归保护和数组管理,可帮助那些需要快速解决复杂问题的项目。

您可以通过一行代码为您的项目安装所有这些合同:

npm i -S openzeppelin-solidity

这将使合同成为一个完美的包装,您可以通过完整的合同路径来引用,就像这样:

import 'openzeppelin-solidity/contracts/token/ERC721/ERC721Full.sol';

总的来说,Zeppelin 在回馈社区方面做得非常出色,提供了非常有价值的、高质量的代码,我们许多人每天都在使用。如果你认为他们应该因他们的行为而受到奖励,那就在你的下一个项目中使用他们的合同,以此来表达你的感激之情。

使用 0x 协议

0x 协议(读作零 x 协议)是一组流行的 API、智能合约和工具,用于构建相互连接的去中心化交易所。你看,许多交易所工作得如此独立,以至于它们失去了共享系统可以提供的很多好处。0x 致力于创建一种交易所可以使用的协议,以便它们拥有共享的流动性池、用户和接口,称为中继。让我们来看看你可以用这种协议构建的主要东西。

构建中继器

中继器是一个 dApp,使用一套共同的工具来与其他中继器共享交易。对于选择特定功能的最佳交易所,他们为用户提供了许多选项,因为它们都分享某些行动,以帮助整个生态系统。

他们使用一个叫做0x.js的库,这个库可以让你与中继器进行高级别、清晰易用的交互。

成为做市商

做市商为交易所提供了外部用户可以根据动态价格进行交易的个人交易。他们是通过利用自己的权力地位从中获利最多,因为他们在任何时刻都能更多地控制哪些交易是有效的。

有了 0x,你可以简单地成为一个做市商,为去中心化交易所提供流动性,使它们在代币高交易网中运作。

0x 协议还有很多功能,你需要自己探索。它是近年来最有趣的项目之一,其代币价格清楚地反映了这一点。如果你对去中心化交易所DAXs)感兴趣,立即开始更多地了解它,加入交易革命。

Aragon

Aragon 是创建运行在完美系统内的去中心化自治组织DAOs)的首选解决方案,而且不需要中介机构。他们为您提供管理公司的工具,直接从您的电脑上操作。我们将探索这些,以便您欣赏其功能的全部潜力,并创建自己的公司在以太坊区块链之上运行。

AragonPM

这是一个工具,用于为他们的 Aragon 客户端分发其自己软件包的不同版本,这样 DAO 可以使用一套固定的改进,而不必不断更新他们的软件需求,可能会破坏他们现有的结构。

AragonOS

这是一个智能合约框架,用于构建具有各种实用功能的去中心化组织,例如控制限制、可升级的合约和插件,您可以根据需要添加。对于希望在区块链上实现强大公司动态的高级 DAO 来说,这是一组极好的智能合约。

AragonJS

这是他们 Aragon 系统的 JavaScript 实现。它允许您创建与去中心化组织一起工作的 dApps,并提供一个漂亮的 API,您可以在几小时内了解。它非常适用于构建自定义界面,以与您的公司进行交互,使您可以根据需要调整。

AragonUI

这是一组 UI 组件,您可以在 JavaScript 应用程序中实现这些组件,以创建具有您希望创建的确切外观的漂亮界面。您不必担心从头设计一切,因为您只需将这些界面元素插入到正确的位置,就可以为项目创建自定义 DAO 实现。

AragonCLI

命令行界面CLI)用于创建和与各种 Aragon 应用程序进行交互,这些应用程序与去中心化组织一起工作。在启动许多不同 DAO 项目时,此 CLI 直观且简单易用。

摘要

在本章中,您已经了解了许多工具,可用于创建高级智能合约应用程序。您首先了解了一些最有用的开发工具指南,这些工具可用于您日常的智能合约开发工作,包括集成开发环境(IDE)、开发框架和测试网络。然后,您继续您的学习之旅,了解了帮助您以高效方式将智能合约与 Web 应用程序集成的以太坊通信工具。之后,您了解了更多关于以太坊基础设施实用程序的信息,这些实用程序位于区块链的较低层,可以更好地访问以太坊区块链的各个方面。

接下来,您通过快速学习了解了安全性的重要性,学习了如何实施审计工具、监控实用程序和测试应用程序,这些工具可以为您提供对代码安全性的全面概述。最后,您通过阅读更多关于可用于帮助您创建各种独特应用程序的最受欢迎的开源库的信息,结束了这一学习路径,这些库具有安全且广受欢迎的代码,被全球成千上万的区块链公司使用。

所有这些信息在正确人手中具有许多危险性,因此成为一名优秀的以太坊开发人员,利用您新获得的知识来改善整个生态系统,而不是利用已有的东西而不提供价值。

在下一章中,我们将探讨各种可以立即实施的 dApp 改进,以提高您的 Truffle 和 React 项目的性能,通过前所未见的技术,真正实现这类 dApp 的最佳性能。

第七章:在测试网上部署

开发智能合约是一项复杂的任务,你需要在不同的环境之间移动,以有效地测试你的应用程序的质量。这就是为什么有许多不同的测试网,在这些测试网上你可以部署你的代码,实验你的合同在不同规则和挖掘算法下的行为,以提高其质量。在本章中,你将了解以太坊主要网络之间的区别,包括 Ropsten、Rinkeby、Kovan 和主网在智能合约保障世界中的定位。

你将了解到每个网络提供的核心挖矿算法变化,从而可以了解你的应用在不同环境中的行为。你还将看到如何为每个网络获取以太币,以便你可以立即在免费的测试网络上开始开发。

在本章中,我们将涵盖以下主题:

  • 使用 Ropsten 进行智能合约开发

  • 了解 Rinkeby 与 PoA

  • 使用 Kovan 进行智能合约开发

  • 介绍主网

使用 Ropsten 进行智能合约开发

每个以太网网络都有一个唯一的标识符,用数字表示所选择的网络,以便以太坊客户端和 Truffle 等框架可以快速选择一个新的测试网络。Ropsten,由 ID 3 标识,是以太坊中使用最广泛的测试网络名称,因为它提供了最接近真实主网的技术堆栈,被真实世界的 dApps 所使用。

请注意,每个测试网络都是一个独立的区块链,具有自己的一套规则和限制,以帮助人们决定在哪里测试他们的 dApps,模拟真实世界的情况。

最初,Ropsten 区块链被命名为Morden,并于 2015 年以太坊启动时部署。一年后,以太坊核心团队决定将 Morden 重命名为Ropsten,以表明它是一个升级版本,具有更好的安全功能和更快的交易性能。

它通过硬分叉不断改进,以包括最新的以太坊版本,以便该区块链与最新的创新保持同步。有趣的是,以太坊的一个最大的升级,被称为Constantinople,首先在该测试网上发布,以便验证其运作方式,然后再将这些改变风险性地应用于主网。在将主网络升级之前,通常会通过硬分叉在测试网上发布颠覆性的以太坊变更,以保证升级的安全性。

因为此网络基于工作量证明PoW),所以容易受到垃圾邮件攻击的影响,少数强大的计算机可以通过 51% 攻击重写区块历史以进行自己的交易。这就是为什么这是测试最不稳定的网络之一,尽管一直在持续改进。事实上,它在 2017 年遭受了垃圾邮件攻击,未知用户生成了大量导致整个区块链崩溃的慢区块,阻止新的交易到达矿工,从而有效地摧毁了网络。

在此事件之后,以太坊基金会收到了来自外部团体的 GPU 捐赠,这些团体希望支持他们的工作。有了这种提高的哈希率,Ropsten 恢复了活力,比以前更加强大,并且仍然运行良好。

Ropsten 的特点

Ropsten 是与主网最相似的区块链,因为它实现了相同的 PoW 挖矿算法,每个人都可以自由生成新区块以换取 Ropsten 以太币,这些以太币没有真实世界的价值。其区块率约为每个区块 30 秒,并且被所有主要的以太坊客户端接受,包括 Geth 和 Parity。

此网络中的以太币可以自由挖掘,就像在主网中一样,并且有几个开放的水龙头可以免费获取以太币。在那些您希望尽可能模拟与主网接近的环境的情况下,此网络最适用,以太币具有真实价值,因此您的合约表现出非常相似的区块率和挖矿性能。事实上,该区块链上的燃气限制通常与主网相同。

获取 Ropsten 以太币

如果您是以太坊的现有用户,则此网络获取以太币的过程非常简单。以下是您必须遵循的步骤:

  1. 如果您尚未这样做,请下载 MetaMask 并通过顶部的按钮将您的网络更改为 Ropsten:

  1. 然后,点击存款按钮,向下滚动,并点击获取以太币以打开水龙头:

  1. 这是 MetaMask 水龙头的外观:

点击请求 1 以太币的水龙头,您将收到一个 MetaMask 通知,批准他们网站上使用您的帐户,以便他们每次点击都可以向您发送一个以太币,最多约五个 Ropsten 以太币。您可以在专用子域中使用 Etherscan 分析您的 Ropsten 交易:ropsten.etherscan.io

在获得 Ropsten 以太币之后,您应该能够使用您的框架或 IDE 连接到该特定网络。以下是将合约部署到此测试网络的步骤,以及对您的 Truffle 配置进行的一些修改:

  1. 要将智能合约部署到此测试网络,您可以修改您的truffle-config.js文件,配置如下:
ropsten: {
  provider: () => new HDWalletProvider(mnemonic, `https://ropsten.infura.io/${infuraKey}`),
  network_id: 3,
  timeoutBlocks: 200, // # of blocks before a deployment times out (minimum/default: 50)
}
  1. 然后,您可以使用以下方式部署您的合约:
$ truffle deploy --network ropsten --reset

记得通过创建一个没有限制的新项目来为 Ropsten 获取有效的 INFURA 密钥。

或者,您可以通过将 MetaMask 网络更改为 Ropsten,使用 Remix IDE 部署到 Ropsten,只要您在其中有以太币。 Remix 将自动重新加载新选择的测试网络。

了解具有权威证明的 Rinkeby

Rinkeby 是最安全的网络之一,用于测试您的应用程序,因为它使用权威证明PoA)来安全生成区块。实际上,它是如此安全稳定,以至于许多人将此网络用于原型、MVP 和演示,因为开发者知道他们的 dApp 将在此链上毫无问题地继续运行。

它由 ID 4 标识,并于 2017 年由以太坊团队创建,为那些想要尝试不同挖矿算法的开发者提供了另一种解决方案。此测试网络速度极快:它每 15 秒生成一个区块。以太币的供应由 puppeth 控制,以防止人们通过滥用挖矿行为生成以太币。

唯一支持的客户端是 Geth,尽管您可以在大多数应用程序中使用 MetaMask 和 Truffle。

描述权威证明

你已经从第三章,以太坊资产中熟悉了 PoA,那里你通过一个基本介绍了如何使用 Puppeth 生成 Clique 网络。Clique 是 Rinkeby 使用的 PoA 算法的名称。它非常类似于 PoS,并且包括选择大约 25 个矿工的小部分,他们充当为链提出新区块的验证者。

每个验证者都对他们想要被接受为下一个区块的区块押注,几秒钟后,被押注以太币量最大的区块被选择。如果验证者在规则范围内表现良好,他们不会失去押注的以太币,但如果他们变得拜占庭,他们将冒着失去押注的风险作为惩罚。

这个共识算法之所以有效,是因为验证者的身份是公开的,这样其他人就知道当一个矿工表现恶意时。为了成为验证者,每个用户都必须将一些敏感数据公开以保护网络。

获取 Rinkeby 以太币

要在此网络中获取以太币,您必须前往 faucet.rinkeby.iowww.rinkeby.io/#faucet,在那里您可以提供带有您地址的社交媒体链接。

这个过程有点混乱,所以这是步骤的分解:

  1. 通过打开 MetaMask 并点击您的地址来复制您的 Rinkeby 以太坊地址:

  1. 前往您的 Twitter 或 Facebook,并创建一个带有您地址但没有其他内容的新推文或帖子;虽然您可以添加文本,但最好只留下您的地址:

  1. 发布推文,点击它,并复制该位置的 URL:

  1. 将该 URL 粘贴到 Rinkeby 的 faucet 页面的输入框中:

  1. 点击“给我 Ether”选择您希望收到的以太币数量。总量每三天限制为 18.75 个 Ether。您将立即收到以太币,但必须等到达该时间才能在将来获得更多以太币。如果一切顺利,您将看到一个绿色的确认消息。如果不行,可能是因为您提供的 URL 无效。确保复制推文本身的 URL:

  1. 确认您在 MetaMask 中收到了您的以太币:

现在,您应该可以在 Rinkeby 上进行操作并在需要时部署您的合约。请记住,这里有一个专门用于探索 Rinkeby 交易的 Etherscan 版本:rinkeby.etherscan.io

要将这个测试网用于您的 Truffle 项目,您需要做以下更改:

  1. 要在这个测试网上部署智能合约,你可以修改你的truffle-config.js文件,配置如下:
rinkeby: {
  provider: () => new HDWalletProvider(mnemonic, `https://rinkeby.infura.io/${infuraKey}`),
  network_id: 4,
  timeoutBlocks: 200, // # of blocks before a deployment times out (minimum/default: 50)
}
  1. 保存了配置文件后,使用以下命令部署您的合约:
$ truffle deploy --network rinkeby --reset

请记得为 Rinkeby 获得有效的 INFURA 密钥,创建一个新项目。

或者,您可以通过将 MetaMask 网络更改为 Rinkeby 在 Remix IDE 中部署,只要您在其中有以太币。 Remix IDE 将自动重新加载已选择的新测试网。

使用 Kovan 进行智能合约开发

Kovan 是由 Parity 团队创建的一个测试网,他们希望能够部署他们的智能合约,知道他们将无休止地运行,因为这个网络非常安全。这个网络是速度最快的,因为它有 4 秒的区块时间,这使得测试变得轻松,因为您不必等待长时间的确认时间。

它诞生是因为 2017 年 Ropsten 受到攻击,当时 Parity 意识到开发者失去了他们的重要工具,因为他们需要在可能最真实的情况下测试他们的智能合约,以模拟区块链的限制。

Kovan 是其中一个最活跃的网络,因为他们提供Kovan 改进提案,即KIPs,用户可以在仓库(github.com/kovan-testnet/kips)上提交 GitHub 问题,介绍他们希望引入到这个网络中的更改。

由于依赖一组受信任的验证者持续以最佳速度生成区块,而不是依赖具有更高节点变异性的公共算法,因此无法对此区块链进行挖掘。您可以在官方 Kovan 白皮书中了解有关为此测试网络生成区块的已批准验证者的更多信息:github.com/kovan-testnet/proposal

此网络的标识符是 ID 42,因为他们决定为可能想要创建的新测试网络保留大量标识符。此网络也不易受到 DDoSing 等攻击的影响,具有大量慢区块的溢出。

如果要连接到此网络,您可以使用 INFURA 或以下 parity 命令:

$ parity --chain kovan

要将智能合约部署到此测试网络,您可以修改您的 truffle-config.js 文件,配置如下所示:

kovan: {
 provider: () => new HDWalletProvider(mnemonic, `https://kovan.infura.io/${infuraKey}`),
 network_id: 42,
 timeoutBlocks: 200, // # of blocks before a deployment times out (minimum/default: 50)
}

使用以下简单命令将您的合约部署到 kovan

truffle deploy --network kovan --reset

使用 Remix 做同样的事情:将 MetaMask 网络更改为 Kovan,IDE 将自动刷新。

获取 Kovan 以太币

获取 Kovan 网络的以太币有两种主要方式:通过项目的公共聊天请求以太币,或者使用一个自动化工具,该工具连接到您的 GitHub 帐户,并在每 24 小时给您一个以太币。

要通过 Gitter 主持的公共聊天请求以太币,请按照以下步骤操作:

  1. 首先访问 Kovan 的公共 Gitter 聊天网站,网址为 gitter.im/kovan-testnet/faucet,界面类似于以下内容:

  1. 使用您的帐户加入该 Gitter 聊天,并在网站底部的聊天框中粘贴您的以太坊地址(从 MetaMask 复制),就像我们在 Rinkeby 中所做的那样:

  1. 按下 Enter 发送您的地址,几分钟后您将收到 1 个 Kovan 以太币。如果您不想等待,可以使用名为 Icarus 的工具获取以太币(在此处提供:faucet.kovan.network/),该工具会自动提供以太币:

  1. 单击使用 GitHub 登录以便他们知道是谁在请求以太币。然后,您将看到一个简单的输入框,以请求每 24 小时一个以太币:

  1. 如果一切顺利,您将在 Etherscan 上看到带有您交易的绿色消息。

一般情况下,如果您想要极快的区块时间并且关心安全性,您应该使用此网络。主要问题是,您无法像 Rinkeby 或 Ropsten 那样轻松获取以太币,因为用户必须拥有 GitHub 资料或访问 Gitter 聊天,即使如此,水龙头每 24 小时也只能提供一个以太币。

介绍 Mainnet

以太坊主网络是创建平台的关键组件,在这个平台上你可以部署智能合约并运行去中心化应用。尽管它是主网络,但一些开发者更喜欢将他们的 dApps 运行在测试网络上,以避免昂贵的成本。无论如何,最好在主网络上部署你的最终应用,这样更多的用户将能够使用你的去中心化应用,使用真正的以太币和最新的更新。

主网络也被称为Homestead,它是在 2015 年随着以太坊的创建而发布的生产版本,所有真实世界的用例都部署在这里。它采用了 15 秒的区块生成时间,这取决于矿工,它运行 PoW,这取决于拥有一个强大的节点网络来维持网络的运行,否则就会因为允许 51% 攻击而变得容易受到攻击。

要获取 Homestead 的以太币,你必须从交易所购买,比如 Coinbase、Kraken、Bittrex,或者接受法定货币的等价物。然后,你必须将你的以太币存储在一个钱包中,这可以是硬件钱包,比如 Trezor,也可以是软件钱包,比如 btc.com 提供的钱包、MetaMask、Mist 或者 myetherwallet.com 提供的钱包。

如果你对安全性非常担心,你甚至可以创建一个纸质钱包来保持它的安全性。总的来说,当涉及到真正的以太币时,你要小心谨慎,因为它包含了可以交换成法定货币和现实世界资源的有价钱值,所以安全是必须的。

摘要

在本章中,你已经学会了了解测试网络。首先,你经历了 Ropsten 的历史,它是如何开始的,以及使其成为以太坊开发者最常用的测试网络的独特特性。然后,你了解了 Rinkeby 测试网络对于那些希望长期依赖测试网络的项目来说是多么强大和安全。你看到了如何通过简单修改 Truffle 配置文件来部署你的合约到每个测试网络。之后,你了解了 Kovan 网络对于那些需要快速确认的开发者来说有多快,实际上它是快速开发的最佳解决方案之一,尽管你已经知道获取 Kovan 的以太币比其他网络更加复杂一些。最后,你探索了 Homestead 网络如何工作,其中包含了只能在交易所购买后才能获得的有价值的以太币。

在下一章中,你将学到更多关于一些最精妙和有价值的开发技巧,你可以立即使用这些技巧来为更高质量的项目创建更好、更快、更高效的 dApps —— 这是你在其他任何地方都找不到的独家信息!

第八章:各种 dApps 集成

本章介绍了如何利用新技术改进现有的 dApps 和智能合约,使它们更快、更好、更有效。有趣的是,大多数 dApps 都可以通过一些技巧进行改进。你将发现 dApp 开发的新方面,包括创建自己的预言机和与智能合约一起工作的后端。首先,你将通过改进你的 React 技能开始,然后我们将转向后端,以便你学会为需要大量资源才能正常工作的混合型 dApps 创建更好的集中式后端。之后,我们将回到前端,学习如何使用 web3.js 构建更强大的 dApps。为了涵盖与你的 dApps 相关的所有领域,你将利用最近获得的关于服务器的知识构建预言机,这是处理预言机时要考虑的主要组件。最后,为了对改进做一个完整的回顾,你将学习如何改进你的开发工作流程,以便以时间和资源为基础生产出最有效的代码。

在本章中,我们将涵盖以下主题:

  • 更好的 React 应用程序

  • 使用 NGINX 构建可扩展的 Node.js 后端

  • 更好的 web3.js dApps

  • 构建你自己的预言机

  • 改进你的开发工作流程

更好的 React 应用程序

你对创建 React 应用程序所需的工作流程很熟悉。但是,许多较新的 dApps 方面更难以控制。这包括智能合约连接性、为 Solidity 函数处理数据以及创建可扩展组件等方面。

正确组织组件

当你的应用程序开始增长时,你希望确保你的代码库足够干净,以支持新的改进而无需在以后重写整个系统。为此,你将从将组件分离到不同文件开始,以便你可以正确地组织你的内容。

例如,看一看名为index.js的这个文件:

import React from 'react'
import ReactDOM from 'react-dom'

class Main extends React.Component { ... }

class ArtContainer extends React.Component { ... }

class ArtPiece extends React.Component { ... }

class Form extends React.Component { ... }

class ButtonContainer extends React.Component { ... }

ReactDOM.render(<Main />, document.querySelector('#root')

你会看到有五个组件都在一个大文件中,由数百行代码组成。这对于只有几个组件的较小项目来说是可以接受的,但是当你开始处理更大的应用程序时,你必须将你的组件分开放在不同的文件中。为此,请为每个组件创建一个具有相同名称的文件。以下是一个示例:

// ArtPiece.js

import React from 'react'
import ReactDOM from 'react-dom'

class ArtPiece extends React.Component { ... }

export default ArtPiece

注意,你必须使用export default关键字导出你的组件,这样你才能得到特定的组件。然后,你的src文件夹最终会看起来类似于这样:

src/
    Main.js
    ArtContainer.js
    ArtPiece.js
    Form.js
    ButtonContainer.js

现在,在你的Main.js组件中,你必须导入所有将要使用的组件。否则,它不会工作。这种重构可以很容易地在任何项目中完成,因为它只是将组件分离到文件中;但是,请确保将它们导入并导出到正确的位置。

动态生成组件

在改进 React dApps 时的另一个技巧是动态生成组件。您可能曾经处于这样一种情况,您必须生成具有不同属性的多个子组件,因为您有某种数组。这似乎很简单,但却非常不直观,因为 React 只能理解其虚拟 HTML 中的某种对象类型。

假设你有一个包含不同动物属性的对象数组,这些属性是从智能合约中获取的:

const myAnimals = [
    {
        name: 'Example',
        type: 'tiger',
        age: 10
    }, {
        name: 'Doge',
        type: 'dog',
        age: 12
    }, {
        name: 'Miaw',
        type: 'cat',
        age: 3
    }
]

您想为这些对象中的每一个生成Animal组件。您不能只是简单地循环它们并创建组件;您必须使用带有普通括号的.map()函数,而不是花括号,因为 React 组件非常挑剔。看看它会是这样:

  1. 首先,您可以按照以下方式设置构造函数来呈现数组中要显示的元素:
import React from 'react'
import ReactDOM from 'react-dom'

class AnimalContainer extends React.Component {
    constructor () {
        super()
        this.state = {
            myAnimals: [
                {
                    name: 'Example',
                    type: 'tiger',
                    age: 10
                }, {
                    name: 'Doge',
                    type: 'dog',
                    age: 12
                }, {
                    name: 'Miaw',
                    type: 'cat',
                    age: 3
                }
            ]
        }
    }
}

ReactDOM.render(<AnimalContainer />, document.querySelector('#root'))
  1. 然后,设置呈现函数以通过map()函数查看所有元素,尽管您可以使用普通的for()循环来生成 JSX 组件数组。请注意,因为 JSX 要求返回动态 HTML 元素,我们将每个元素返回在普通的()括号内而不是花括号{}内:
render () {
    return (
        <div>
            {this.state.myAnimals.map(element => (
                <Animal
                    name={element.name}
                    type={element.type}
                    age={element.age}
                />
            ))}
        </div>
    )
}
  1. 最后,创建Animal组件以在您的 dApp 上显示它:
class Animal extends React.Component {
    constructor () {
        super()
    }

    render () {
        return (
            <div>
                <div>Name: {this.props.name}</div>
                <div>Type: {this.props.name}</div>
                <div>Age: {this.props.name}</div>
            </div>
        )
    }
}

正如你所见,AnimalContainer组件正在使用.map()函数动态生成Animal。这就是如何将 JavaScript 对象转换为 React 组件。请注意,我们是在 render 函数内生成组件的,并且.map()函数块在普通括号中而不是花括号中:

.map(element => ())

更快地启动项目

React 项目的另一个问题是,您必须始终从头安装依赖项,设置webpack文件,并确保一切正常运行。这很烦人,耗费了太多宝贵的时间。为了解决这个问题,有create-react-app库,尽管它添加了许多不必要的包,可能会在以后造成麻烦,增加了可升级性更困难,因为它基于封闭系统。

最好尽可能使用最简化的版本启动 React 项目。这就是我创建的开源dapp项目,其中包含了最小、最精简的使用 Truffle 启动 React dApp 项目的版本,让您可以立即开始。您可以使用以下代码从我的 GitHub 上获取最新版本:

$ git clone https://github.com/merlox/dapp

然后使用npm i安装所有依赖项,运行webpack watch以在进行更改时保持文件捆绑为webpack -d -w,并在dist/文件夹中运行您选择的静态服务器。例如,您可能会选择http-server dist/

dapp项目正在为您执行以下任务,以便您可以立即开始新的 dApp 工作:

  • 安装所有 react, webpack, babel, 和 truffle 的依赖项。刚刚好,因为它甚至不包括 .css 加载器,这样你就可以轻松管理你的包。如果你想使用它,你仍然需要全局安装 Truffle。

  • 为你设置 webpack.config.js 文件,其中入口为 /src/index.js,输出为 /dist/,并使用加载器加载所有 .js.html 文件。

  • 设置最简单的 HTML 和 JavaScript 索引文件。

因此,每当你需要启动一个新项目时,你可以简单地克隆 dapp 存储库以加快启动速度。

带有 NGINX 的可伸缩 Node.js 后端

Node.js 是创建命令行应用程序、服务器、实时后端以及各种用于开发 Web 应用程序的工具中最强大的工具之一。它之所以美妙,是因为 Node.js 就是服务器上的 JavaScript,这与你的 React 前端很好地结合在一起,实现了 JavaScript 到处都是。即使它是集中式的,你也会在许多场合使用它,用于去中心化项目,你无法摆脱以太坊区块链的限制。你看,Solidity 和 Vyper 的限制很严重:除了基本的基于函数的代码之外,你几乎什么都做不了。迟早你都得使用集中式后端来实现像需要仪表板这样的高级应用程序。

至少在去中心化的主机和存储解决方案显着改进之前,我们将不得不使用集中式后端来处理那些不能轻松通过智能合约完成的特定任务。

NGINX(发音为engine X),另一方面,是一个可以用作反向代理和负载均衡器等的 Web 服务器。与 Node.js 结合使用是一个神奇的工具,因为它加速了后端调用并显着提高了可伸缩性。简单来说,NGINX 是大量用户需要最佳性能的高级项目的 Node.js 的最佳伙伴。这并不意味着它不能用于简单的 Node.js 应用程序,绝对不是这样:NGINX 也非常适用于小型应用程序,帮助你轻松控制端口并理解域名。你将学到所有必要的知识,以便正确地为更大的 dApps 使用它。

我们将从学习如何创建一个带有 NGINX 后端的 Node.js 应用程序开始,然后我们将把它连接到一个真实的域名,最终部署一个可扩展的 NGINX 后端,包括负载均衡等其他改进。

创建一个 Node.js 服务器

你可以在任何地方创建 Node.js 应用程序,但迟早你都得将该应用程序迁移到真实的托管服务,比如 亚马逊云服务 EC2AWS EC2)或 DigitalOcean。两者都是优秀的选择,所以我们将探讨如何部署到 DigitalOcean。

无论如何,我们将首先在本地创建 Node.js 服务器,然后将其移动到托管解决方案中。假设我们有以下情景:你刚刚使用 React 创建了一个功能完善且效率非常高的 dApp,因此你希望其他人能够免费使用这个应用程序。你可以将其部署到 GitHub 页面或 HostGator 提供的静态托管网站等,但你希望扩展应用程序的功能,并具有仅对特定用户可访问的数据库和管理页面。这就是你需要自定义服务器和虚拟专用服务器VPS)的地方,它基本上是一个远程计算机,你可以在其中进行自定义服务器的创建,通常使用 Linux 操作系统。

要实现这一切,你必须首先创建一个 Node.js 服务器,为你提供静态文件,而不是使用诸如http-server之类的工具。让我们从在前几章中创建的 Social Music 应用程序创建一个静态服务器开始吧。继续在项目目录内创建server/public/文件夹,并将基本代码移动到public文件夹中:

我们将所有与节点相关的文件(例如package.json)和与 GitHub 相关的文件(例如LICENSE)移动到了与服务器文件分开的地方,以便进行组织。

首先,在server/中创建一个名为server.js的文件,作为设置服务器所需的主要文件,包括所需的库:

const express = require('express')
const bodyParser = require('body-parser')
const app = express()
const path = require('path')
const port = 9000
const distFolder = path.join(__dirname, '../public', 'dist')

然后,配置服务器监听器,负责在外部用户请求时提供正确的文件:

app.use(distFolder, express.static(distFolder))
app.use(bodyParser.json())

app.use((req, res, next) => {
   console.log(`${req.method} Request to ${req.originalUrl}`)
   next()
})
app.get('*/bundle.js', (req, res) => {
   res.sendFile(path.join(distFolder, 'bundle.js'))
})
app.get('*', (req, res) => {
   res.sendFile(path.join(distFolder, 'index.html'))
})

app.listen(port, '0.0.0.0', (req, res) => {
    console.log(`Server listening on localhost:${port}`)
})

首先我们导入了expressbody-parser。Express 是一个使用 Node.js 创建 web 服务器的框架,而 body-parser 则用于处理所有我们的 JSON 请求,以便能够理解这些类型的消息,因为默认情况下,Node.js 无法理解 JavaScript 请求的json对象。然后,我创建了几个get请求处理程序,以便在从dist文件夹请求时发送bundle.js文件和index.htmlapp.use()是一个中间件,这意味着它接收所有请求,进行一些处理,并让其他请求块继续执行它们的工作。在这种情况下,我们使用该中间件来记录有关每个请求的信息,以便在出现任何问题时调试服务器。

使用以下命令安装所需的服务器依赖项:

npm i -S body-parser express

现在,你可以运行服务器了:

node server/server.js

上述命令的问题在于,每当出现错误请求或者对服务器文件进行更改时,你都必须重新启动服务器。对于开发工作,最好使用nodemon实用程序,它会自动刷新服务器。使用以下代码安装它:

npm i -g nodemon

然后,再次运行你的服务器:

nodemon server/server.js

为了更轻松地进行开发,在你的package.json文件中创建一个新的脚本,以便更快地运行该命令:

{
  "name": "dapp",
  "version": "1.0.0",
  "description": "",
  "main": "truffle-config.js",
  "directories": {
    "test": "test"
  },
 "scripts": {
 "server": "nodemon server/server.js"
 },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@babel/core": "⁷.2.2",
    "@babel/preset-env": "⁷.3.1",
    "@babel/preset-react": "⁷.0.0",
    "babel-loader": "⁸.0.2",
    "babel-polyfill": "⁶.26.0",
    "body-parser": "¹.18.3",
    "css-loader": "².1.0",
    "express": "⁴.16.4",
    "html-loader": "⁰.5.5",
    "html-webpack-plugin": "³.2.0",
    "react": "¹⁶.8.1",
    "react-dom": "¹⁶.8.1",
    "style-loader": "⁰.23.1",
    "truffle-hdwallet-provider": "¹.0.3",
    "web3": "¹.0.0-beta.46",
    "webpack": "⁴.29.3",
    "webpack-cli": "³.2.3"
  }
}

然后,你可以运行npm run server来启动服务器:

获取托管解决方案

现在我们的静态服务器已经运行,我们可以将其部署到托管解决方案上,以使我们的 dApp 从外部世界访问。在此之前,将您的项目添加到 GitHub,并包含所有最新更改,以便稍后在另一台计算机上使用。转到digitalocean.com,并使用链接m.do.co/c/db9317c010bb创建帐户,这将为您提供价值 $100 的服务,免费使用 60 天,并在您添加 $25 到服务时额外获得 $25。这将足以至少运行一个基本的 VPS 服务器 3 个月。您需要添加信用卡/借记卡或添加 $5 的 PayPal 美元才能开始使用它。在我的情况下,我支付了 $5 美元。

转到 Droplets 部分,然后单击“创建 Droplet”:

然后,您必须选择在该服务器内安装哪个发行版;此发行版称为 Droplets。您可以选择使用 Node.js 的一键式安装,但我认为在没有用户界面时了解如何从头安装 Node.js 非常重要。因此,请选择 Ubuntu 18.04 作为操作系统,并选择每月 $5 的计划:

选择距离您所在地最近的数据中心以获得最佳性能。我住在西班牙,所以我会选择德国或英国的服务器。对您来说,可能会有所不同:

将其余选项保持不变,然后按“创建”进行创建。您将看到实时创建的过程。单击您的 droplet 并复制 IPv4 地址,您将需要连接到该服务器:

在 VPS 主机上设置您的服务器

如果您使用的是 Windows,请从官方网站下载 PuTTY 以连接到外部服务器,网址在这里:www.putty.org。安装后打开它:

将您的 IP 地址粘贴到主机名输入框中,并通过单击“打开”连接到它。它会警告您连接到一个未知的服务器;只需点击“是”。然后,它会要求您登录;将root作为您的默认用户名:对于每个托管提供商,它都是不同的。

如果您使用的是 Mac,则可以简单地使用以下命令而不是使用 PuTTY:

ssh root@<your-ip>

尽管 root 是 DigitalOcean 提供的默认用户,但请注意每个托管解决方案可能会有所不同,因此请查看其网站上提供的信息。

然后,它会要求您输入密码,您可以通过电子邮件获得;由于 DigitalOcean 已经向您发送了登录凭据,因此您在粘贴时将不会看到它,这是一种安全措施。您可以通过右键单击粘贴,除此之外什么都不要做,因为 PuTTY 就是这样工作的。

紧接着,它将要求您重新输入当前密码,然后将您的 Unix 密码更改为新密码,因为您不能依赖自动生成的密码:

现在您应该可以访问您的服务器。正如您所看到的,除了您的命令行工具之外,您没有用户界面。您不希望像 root 用户一样执行所有任务,因为这是一个安全风险,任何操作都可以无限制地访问整个系统。使用以下代码创建一个新用户:

useradd -m <your-user-name>

以下是一个示例:useradd -m merunas-m 标志将创建一个 /home/merunas 用户文件夹。然后,使用 su merunas 或您创建的任何用户切换到该用户。su 命令表示“切换用户”。

您还必须使用 passwd 命令设置密码,否则您将无法在会话开始时登录。例如,您可以使用此命令:passwd merunas。您下次想要以该用户登录,以避免作为 root 用户的潜在安全风险。然后,您将想要将您的 shell 更改为 Bash 而不是 sh,以便在按 ab 时获得自动完成,以及其他帮助您编写命令的实用程序。请使用以下命令执行此操作:

chsh <your-user> -s /bin/bash

然后,将您的用户添加到 sudo 组中,以便能够以 root 用户身份运行命令而无需更改用户。您必须以 root 用户身份运行此命令:

usermod -aG sudo <your-user>

这是一个示例:usermod -aG sudo merunas

现在我们要做的是从头开始安装 Node.js 和 NGINX。Node.js 的过程有点复杂,因为他们不断改进他们的软件,所以设置起来更困难,但完全可行。转到 nodejs.org/en/download/current/ 并通过右键单击其中的按钮复制源代码的链接地址:

返回到 PuTTY 会话并运行 wget 命令与源代码链接一起下载节点二进制文件,以便您可以安装它:

wget https://nodejs.org/dist/v11.10.0/node-v11.10.0.tar.gz

使用 tar 进行提取,如下命令行所示:

tar -xf node-v11.10.0.tar.gz

运行 cd node-v11.10.0 切换到当前目录。要从该文件夹安装 Node.js,您需要安装一些依赖项,这些依赖项可以通过名为 build-common 的软件包安装:

sudo apt install build-common

然后,运行 ./configuresudo make 命令来运行安装。make 命令生成所需的配置,但需要几分钟的时间,所以请耐心等待。以前,您还必须运行 sudo ./install.sh,但现在不再需要;您仍然可以获得漂亮的 node 可执行文件。只需将其复制到二进制文件位置以便能够全局使用:

sudo cp node /bin

您现在可以删除安装文件夹和下载的文件。或者,您可以使用 sudo apt install nodejs 来安装 Node.js,但这是一个过时的版本,不如官方二进制文件维护得好。现在您已经安装了 Node.js,请从 GitHub 克隆您的社交音乐项目,或者使用以下命令使用我的:

git clone https://github.com/merlox/social-music

sudo apt install npm在外部安装npm,以便你可以安装数据包。你必须从另一个来源获取它,因为 Node.js 不包含它。npm 的好处是你可以立即使用sudo npm i -g npm将其更新到最新版本,因此无论你从哪里获取哪个版本,都不重要,你都可以轻松地将其更新到最新版本,而不需要经过漫长的过程。

现在,你可以运行npm install来安装你在social-music项目中的依赖项。检查你的package.json文件是否包含你之前创建的npm run server命令。否则,使用vim或任何其他文本编辑器,如nano,再次添加它:

"scripts": {
    "server": "node server/server.js"
}

当你使用npm run server命令时,你会发现你的服务器正常运行;问题在于你不应该使用nodemon,因为它是为开发而设计的,没有考虑到在不同环境下可能出现的问题。

出于这个原因,你有一个非常适合在生产环境中使用的实用工具。它叫做pm2,它会保持你的服务器在线,即使在某个时间点发生严重错误。这个实用工具非常好,因为你可以监控你的服务器并运行不同服务的各种实例。使用以下命令在全局安装它:

sudo npm i -g pm2

它非常容易使用。你只需运行pm2 start server/server.js就可以使服务成为守护进程,这意味着无论出于什么原因停止运行,都会重新启动它。要停止它,从运行服务的列表中使用pm2 delete server

恭喜!你的 Node.js 应用程序正在你的服务器上运行。现在,为了让它对世界可用,你必须将它暴露在80端口上,这是所有网站使用的公共端口。你可以通过修改你的server.js文件或使用所谓的前端服务器来实现,该服务器接收所有请求并将它们重定向到正确的位置。在我们的情况下,那将是 NGINX。但在此之前,我们需要一个易于访问的域名,这将使我们的 IP 管理更加轻松。

获取一个域名

你需要一个域名来帮助人们通过一个易于记忆的名称访问你的网站,而不是在他们的浏览器上输入一个长长的 IP 数字。只需进行一些更改,域名将与你的托管解决方案关联起来。要获得一个域名,请访问godaddy.com并搜索你想要的名称:

选择最适合你业务的领域:

点击“添加到购物车”按钮购买,并创建一个账户(如果你没有账户)。我总是使用 PayPal,因为它更容易管理。几分钟后,你的域名将在你的仪表板上可用:

现在,你可以转到你的 DNS 管理设置,将你的域名指向你托管的服务器,以便从该名称访问:

点击 A 记录旁边的铅笔图标,并将指针更改为您的 IP 地址,如下所示:

通过这一变化,您现在可以使用域名而不是 IP 连接到您的服务器,在 Mac 中,例如这样:

ssh [email protected]

它将与以前完全相同的方式工作。您还可以在端口 80 上启动 Node.js 服务器,并且您将能够使用该域名访问网站。但是,当涉及与域名通信时,Node.js 受到了限制,因此我们必须使用更高级的解决方案。

设置 NGINX

现在您的域名已经设置好了,是时候将 NGINX 配置为前端服务器,以将您的域名与 Node.js 实例连接起来了。NGINX 将为您处理所有请求,这样您就可以专注于改进您的 Node.js 应用程序。

与之前一样连接到服务器,并使用以下命令安装nginx

sudo apt install nginx

之后,您将需要编辑 NGINX 的配置文件,位于/etc/nginx/sites-enabled/default。只需用vim编辑您的默认文件:

sudo vim /etc/nginx/sites-enabled/default

然后,添加以下代码以能够在 Node.js 服务器中使用域名:

upstream nodejs {
  server socialmusic.online:9000;
}

server {
  listen 80;
  server_name socialmusic.online;

  gzip on;
  gzip_comp_level 6;
  gzip_vary on;
  gzip_min_length 1000;
  gzip_proxied any;
  gzip_types text/plain text/html text/css application/json text/JavaScript;
  gzip_buffers 16 8k;

  location / {
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection 'upgrade';
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;

    proxy_pass http://nodejs;
  }

  location ~ ^/(images/|img/|JavaScript/|js/|css/|stylesheets/|static/) {
    root /home/merunas/social-music/public;
    access_log off;
    expires max;
  }
}

首先,我们定义了一个upstream块。在这里,我们告诉 NGINX 我们在正确端口上运行的node.js服务器的位置。这对于保护端口 80 非常重要,因为大部分请求都会在那里执行。

然后,我们创建了一个server块。这些类型的块用于在定义的端口上设置一些配置。listen 80;语句告诉 NGINX 在该服务器块内处理端口 80 的请求。然后,我们为更快的加载时间添加了一些gzip压缩,以及一个将所有请求传递给upstream nodejs的位置块。另一个位置块是用于提供静态文件的,以便您有图像之类的文件,因为这是传递静态内容的更快方式。请注意,root /home/merunas/social-music/public;根位置是我们的静态文件的位置。

请记得将socialmusic.online改为您的域名。您现在可以使用以下命令行运行 NGINX:

sudo service nginx restart

这将重新启动服务,使其在后台保持运行。您的网站现在可以在任何浏览器中使用您的域名访问。要完成部署,我们将添加 SSL。SSL是用于保护访问您的 dApp 的通信的加密算法。这是非常常见的,并且对于任何重要项目来说都必须添加。

添加 SSL 安全性

要安装 SSL,我们将使用Let's Encrypt的免费证书,这是一个非营利性组织,其目标是为每个人提供免费 SSL 证书来保护互联网。以下是步骤:

  1. 安装以下库:
sudo apt install software-properties-common certbot python-certbot-nginx
  1. 运行certbot应用程序向您的 NGINX 服务器添加:
sudo certbot --nginx
  1. 提供您的电子邮件地址,接受服务条款,并选择 1 作为您的域名:

  1. 它会询问您是否想要将所有请求重定向到 443 安全的 HTTPS 端口。只需通过选择第二个选项说是:

就这样!现在,所有请求都启用了 HTTPS,并且您的域将自动重定向到 HTTPS。这可以手动完成,但这种方式要简单得多,这样您在处理这些类型的复杂身份验证系统时就可以避免无数的头痛。

现在,您的去中心化应用程序使用了一个 NGINX 服务器,并且启用了 HTTPS,使用了一个 Node.js 集中式后端,您可以根据需要扩展其功能,这些功能在简单的智能合约中无法实现。两全其美。

更好的 web3.js dApps

web3.js 是您的 Web 应用程序中与智能合约通信的最常用实用程序,可以将它们转换为去中心化应用程序。它能够管理无尽的交易,并且一旦设置完成就会自动工作。

问题在于许多 web3.js 应用程序并没有被优化,至少不尽可能好。因为我们正在处理智能合约,所以代码很快就会变得混乱,这使得中长期内的维护变得更加困难。因此,重要的是您从一开始就研究系统,以创建更好的 web3.js dApps,学习使您在与智能合约交互时成为更好的程序员的技巧和诀窍。

你将会使用很多基于 web3 的 dApps,那么为什么不学习做事情的最佳方式,以便在长期内减少头痛,并创建更高质量的代码呢?以下是一些使 web3.js dApps 更好的技巧和诀窍。

设置固定的 web3.js 版本

如果您之前使用过 MetaMask,您可能已经注意到它会将 web3.js 注入到您访问的每个页面中,因为它需要 web3.js 才能与智能合约进行交互。这很好:这是一种预期的行为,但通常会导致旧版本的 web3.js,主要是版本 0.20,这个版本在 web3.js 1.0 推出几年后仍在被使用。他们不想强迫用户更新到最新版本,因为那样会破坏许多已经依赖于 MetaMask 的 web3.js dApps;这是一个巨大的潜在问题。

这就是为什么非常必要为您的项目设置一个固定的 web3.js 版本,以便您不依赖于 MetaMask 或任何其他以太坊客户端强制您使用的版本。必须提供某种保证,即您的 dApp 将在未来继续正常工作。

要做到这一点,请看一下这段代码:

import NewWeb3 from 'web3'

window.addEventListener('load', () => {
    window.web3Instance = new NewWeb3(NewWeb3.givenProvider)
})

我们在这个示例中使用 web3.js 1.0。接下来,我们导入NewWeb3类,这只是一个不同的名称,用于区分它与 MetaMask 提供的Web3。设置一个新的 web3 对象来使用我们特定版本的 web3 与区块链通信。它称为 web3Instance 而不是普通的 web3,因为我们希望使用不同的名称来避免使用 MetaMask 提供的名称。你看,我们不知道 MetaMask 何时会注入自己的 web3 版本,因此,我们确保使用不同的名称来保证我们的版本已经设置好了。然后,我们使用 window 对象设置了一个全局的 web3Instance 变量,以便它可以在我们应用程序的任何地方访问,并且我们在页面加载后通过监听事件'load'来实现。

在项目中尝试一下,你会发现web3Instance是你在导入中定义的版本。请注意,.givenProvider正在从 MetaMask 中注入的 web3.js 数据中获取数据以设置一个新的 web3.js 版本。确保在所有未来的项目中使用这个提示,以确保你的 dApp 适用于未来和过去的 web3.js 版本,因为 MetaMask 不断地以不可靠的方式更改它自己的系统。

创建帮助函数

帮助函数是那些帮助你轻松管理更复杂的函数的函数。它们本质上是帮助其他函数的函数,其中一些通用逻辑可以帮助你避免反复编写代码。

这些是重要的函数,因为它们会极大地提高代码的可维护性。你将能够在更少的行数中看到发生的事情,并且你将能够更快地升级你的代码。

例如,在 web3.js 1.0 中,合约必须为每个智能合约调用和事务使用一行较大的代码:

await this.state.contractInstance.methods.functionName(parameters).send({from: this.state.userAddress})

这个描述性的名称比必要的稍长一点。让我们用一个帮助函数来缩短它:

async function send(functionName, parameters) {
    await this.state.contractInstance.methodsfunctionName.send({from: this.state.userAddress})
}

正如你所见,我们已经将一种方法转换成了括号版本,因为这是你可以使用唯一参数动态生成函数名称的方法。过去,我记得使用以下快捷方式来快速选择元素,而不必一遍又一遍地输入相同的结构:

function q(element) {
    return document.querySelector(element)
}

有了这样一个简单的帮助函数,我将一个 22 个字符的函数转换成了一个具有相同逻辑的 1 个字符的函数。起初可能看起来荒谬,但当你在一个项目中需要使用它 100 次时,你会意识到你大大减少了代码的大小,并且使其更易于阅读。你实际上节省了 2,200 行代码。现在这就是用最小的改变来提高效率!

使你的函数变成 Promise

现代 JavaScript 使用 promises 来清晰地处理事务,因为它让你选择使用同一个函数同步或异步地运行代码,而不是使用回调函数,其中你必须堆叠代码层来控制事物的流程。

这就是为什么如果你的回调函数还没有转换成 promises,那么所有的回调函数都必须被转换成 promises。对于最新版本的 web3 来说,这不是问题,但对于 web3.js 0.20 和许多其他需要使用回调函数的库来说,最好将它们转换为 promises,以便更轻松地编写代码。

有一个名为bluebird的库可以帮助你实现这一点,它会将对象内的所有函数转换为 promises。使用以下命令进行安装:

npm i -S bluebird

使用以下内容将其导入到你的 React 项目中:

import * as Promise from 'bluebird'

使用以下函数将你的对象方法转换为Async

web3Instance = Promise.promisifyAll(web3Instance)

然后,你可以使用Async函数而不是回调函数,就像这样:

web3Instance.eth.getAccountsAsync()

// Instead of 
web3Instance.eth.getAccounts()

这只是一个例子:你只需在你的回调函数中添加Async关键字,就可以使用 promise 版本,而不需要做其他任何事情。

使用 web3.js 监听事件

事件对于管理你的去中心化应用程序的流程至关重要,因为你可以实时获取关于智能合约中发生的变化的更新,并相应地采取行动。你甚至可以创建 Node.js 应用程序来通知你有关关键变化的信息。

例如,假设你运行一个银行智能合约,并且有一个事件在你的智能合约中的资金达到临界的 10 以太币时被激活:

contract Bank {
    event CriticalLow(uint256 contractBalance);
    ...
}

你想要被通知这些变化,所以你在一个node.js实例上建立了一个简单的 web3.js dApp,当发生这种情况时会给你发送一封电子邮件:

// Node.js

function sendCriticalEmail() {
    // Sends an email when something critical happens to fix it ASAP
}

function listenToCriticalLow() {
    // Listen to critical events on real-time
}

这可能是一个监控系统,你自己设置的,用来管理一个被数百万用户使用的 dApp,以使其尽可能长时间地保持运行状态。你可以说,在这样的场景中监听事件是必不可少的,那么你该如何做呢?这是基本结构:

function listenToCriticalLow() {
    const subscription = web3.eth.subscribe('CriticalLow', {
        address: <your-contract-address>
    }, (err, result) => {
        if(!err) sendCriticalEmail()
    })
}

当事件生成时,你的web3.eth.subscription函数将执行回调。这基本上就是你如何在 web3 中监听事件的方法。现在,你知道如何在你的 dApp 工作流程中运用它们了。

构建你自己的 Oracle

Oracle 是外部应用程序,它们帮助你的智能合约从外部世界获取信息,以执行一些 Solidity 或 Vyper 中不可能实现的功能。它们的工作原理很简单:你创建一个中心化的服务器,在需要时调用你的智能合约的特定函数。

它们用于生成随机数、提供实时价格数据和显示网站信息。正如你所知,智能合约无法生成随机数,因为在区块链中不能存在任何关于能够避免意外情况的不确定性。

在这一部分,你将学习如何创建一个 Oracle 来为区块链上的游戏生成一个 1 到 100 之间的随机数。已经有一些做这些任务的 Oracle,即 Oraclize,它已经在 Solidity 中被使用了很长时间。

构建随机生成的 Oracle

Oracle 是智能合约从外部世界获取信息的一种方式。它们是中心化服务器、外部区块链和 API 与运行在以太坊上的智能合约之间的桥梁。基本上,它们是一种服务,可以为您提供来自无法通过普通智能合约访问的地方的重要信息,它们通过设置一个中心化服务器监听您的合约的 web3 事件来工作。

首先,创建一个名为oracle的新项目,运行truffle init以便编译合约,使用npm init -y设置 npm,并创建一个生成事件并处理Oracle.sol的智能合约:

pragma solidity 0.5.4;

contract Oracle {
    event GenerateRandom(uint256 sequence, uint256 timestamp);
    event ShowRandomNumber(uint256 sequence, uint256 number);
    uint256 public sequence = 0;

    function generateRandom() public {
        emit GenerateRandom(sequence, now);
        sequence += 1;
    }

    function __callback(uint256 _sequence, uint256 generatedNumber) public {
        emit ShowRandomNumber(_sequence, generatedNumber);
    }
}

这很基本:当用户通过调用generateRandom()函数请求时,执行带有随机生成的数字的__callback()函数的想法。我们将设置一个事件监听器,在适当的时间给用户提供随机数,带有正确的序列标识符。

记得更新您migrations文件夹中的1_initial_migrations.js文件,以告诉 Truffle 部署正确的合约:

var Oracle = artifacts.require("./Oracle.sol")

module.exports = function(deployer) {
    deployer.deploy(Oracle)
}

然后,通过在truffle-config.js中设置正确的配置来将其部署到ropsten。您已经知道如何做到这一点,因为我们在之前的章节中学习了如何在 Truffle 的配置文件中设置 Infura 以用于 Ropsten:

truffle deploy --network ropsten --reset

现在,我们可以创建一个 Node.js 应用程序,该应用程序监听由我们的智能合约生成的事件,并使用以下代码在一个oracle.js文件中启动生成一个随机生成的数字类型的正确请求:

const Web3 = require('web3')
const fs = require('fs')
const path = require('path')
const infura = 'wss://ropsten.infura.io/ws/v3/f7b2c280f3f440728c2b5458b41c663d'
let contractAddress
let contractInstance
let web3
let privateKey
let myAddress

我们已经导入了web3fspath作为与合约交互的库。然后,我们定义了一个用于连接到 Ropsten 部署和与合约交互的 websockets Infura URL。重要的是您使用wss而不是http,因为这是接收事件的唯一方式。最后,我们添加了一些稍后会需要的全局变量。

通过创建和签署使用我们的私钥的自定义交易对象,我们可以生成没有 MetaMask 的交易。我们可以使用以下基于.secret文件中的助记词生成私钥的函数:

// To generate the private key and address needed to sign transactions
function generateAddressesFromSeed(seed) {
    let bip39 = require("bip39");
    let hdkey = require('ethereumjs-wallet/hdkey');
    let hdwallet = hdkey.fromMasterSeed(bip39.mnemonicToSeed(seed));
    let wallet_hdpath = "m/44'/60'/0'/0/0";
    let wallet = hdwallet.derivePath(wallet_hdpath).getWallet();
    let address = '0x' + wallet.getAddress().toString("hex");
    let myPrivateKey = wallet.getPrivateKey().toString("hex");
    myAddress = address
    privateKey = '0x' + myPrivateKey
}

这相当复杂,尽管我们只需要专注于安装和导入bip39ethereumjs-wallet库以生成用于签署交易的privateKey。我们可以使用以下命令安装依赖项:

npm i -S bip39 ethereumjs-wallet web3

然后,我们可以创建一个start函数,该函数将设置所需的合约并开始监听正确的触发事件以调用__callback()函数:

// Setup web3 and start listening to events
function start() {
    const mnemonic = fs.readFileSync(".secret").toString().trim()
    generateAddressesFromSeed(mnemonic)

    // Note that we use the WebsocketProvider because the previous HttpProvider is outdated and doesn't allow subscriptions
    web3 = new Web3(new Web3.providers.WebsocketProvider(infura))
    const ABI = JSON.parse(fs.readFileSync(path.join(__dirname, 'build', 'contracts', 'Oracle.json')))
    contractAddress = ABI.networks['3'].address
    contractInstance = new web3.eth.Contract(ABI.abi, contractAddress)

    console.log('Listening to events...')

    // Listen to the generate random event for executing the __callback() function
    const subscription = contractInstance.events.GenerateRandom()
    subscription.on('data', newEvent => {
        callback(newEvent.returnValues.sequence)
    })
}

首先,我们读取助记词的 12 个单词的密码来使用先前的generateAddressesFromSeed()函数生成我们的 privateKey 和 address。然后,我们使用WebsocketProvider为我们的 Ropsten Infura URL 设置一个新的 web3 实例,因为我们无法使用HttpProvider监听事件。之后,我们通过从 Truffle 生成的包含部署合约地址的 JSON 文件中读取 ABI 数据来设置contractInstance

最后,我们使用contractInstance.events.GenerateRandom()函数为GenerateRandom事件设置了一个订阅,这将使用与之对应的序列调用callback()函数。让我们看看回调函数是什么样子的。请记住,这个函数将运行我们智能合约的__callback()函数,以向用户提供一个随机生成的数字,因为我们不能直接使用 Solidity 生成随机数:

// To generate random numbers between 1 and 100 and execute the __callback function from the smart contract
function callback(sequence) {
    const generatedNumber = Math.floor(Math.random() * 100 + 1)

    const encodedCallback = contractInstance.methods.__callback(sequence, generatedNumber).encodeABI()
    const tx = {
        from: myAddress,
        gas: 6e6,
        gasPrice: 5,
        to: contractAddress,
        data: encodedCallback,
        chainId: 3
    }

    web3.eth.accounts.signTransaction(tx, privateKey).then(signed => {
        console.log('Generating transaction...')
        web3.eth.sendSignedTransaction(signed.rawTransaction)
            .on('receipt', result => {
                console.log('Callback transaction confirmed!')
            })
            .catch(error => console.log(error))
    })
}

这个函数接收序列参数以将值映射到正确的 ID,使用户能够确定哪个事件适合他们。首先,我们使用Math.random()生成一个介于 1 和 100 之间的随机数,通过一些计算来使其适应我们期望的范围。然后,我们生成一个称为tx的交易对象,其中包括我们的函数数据,包括sequencegeneratedNumber,以及一些基本参数,如gasfrom地址。最后,我们通过首先使用privateKey进行签名,然后使用web3.eth.sendSignedTransaction发送该交易到我们的Oracle智能合约。当矿工确认时,我们会看到console.log显示"Callback transaction confirmed!",或者在出现问题时显示错误。

就是这样了!我们可以在底部添加start()函数初始化以开始监听事件。以下是完整的代码:

  1. 在文件开头导入所需的库并设置将在项目中使用的变量:
const Web3 = require('web3')
const fs = require('fs')
const path = require('path')
const infura = 'wss://ropsten.infura.io/ws/v3/f7b2c280f3f440728c2b5458b41c663d'
let contractAddress
let contractInstance
let web3
let privateKey
let myAddress
  1. 创建generateAddressesFromSeed()函数,它为您提供了访问给定种子中包含的帐户的权限:
// To generate the private key and address needed to sign transactions
function generateAddressesFromSeed(seed) {
    let bip39 = require("bip39");
    let hdkey = require('ethereumjs-wallet/hdkey');
    let hdwallet = hdkey.fromMasterSeed(bip39.mnemonicToSeed(seed));
    let wallet_hdpath = "m/44'/60'/0'/0/0";
    let wallet = hdwallet.derivePath(wallet_hdpath).getWallet();
    let address = '0x' + wallet.getAddress().toString("hex");
    let myPrivateKey = wallet.getPrivateKey().toString("hex");
    myAddress = address
    privateKey = '0x' + myPrivateKey
}
  1. 创建start函数以设置 web3 监听器:

// Setup web3 and start listening to events
function start() {
    const mnemonic = fs.readFileSync(".secret").toString().trim()
    generateAddressesFromSeed(mnemonic)

    // Note that we use the WebsocketProvider because the previous HttpProvider is outdated and doesn't allow subscriptions
    web3 = new Web3(new Web3.providers.WebsocketProvider(infura))
    const ABI = JSON.parse(fs.readFileSync(path.join(__dirname, 'build', 'contracts', 'Oracle.json')))
    contractAddress = ABI.networks['3'].address
    contractInstance = new web3.eth.Contract(ABI.abi, contractAddress)

    console.log('Listening to events...')

    // Listen to the generate random event for executing the __callback() function
    const subscription = contractInstance.events.GenerateRandom()
    subscription.on('data', newEvent => {
        callback(newEvent.returnValues.sequence)
    })
}
  1. 最后,创建执行智能合约中的__callback()函数的回调函数。函数名称以两个下划线开头,以避免调用现有函数,因为它是一个专门由 Oracle 专用的特殊函数:
// To generate random numbers between 1 and 100 and execute the __callback function from the smart contract
function callback(sequence) {
    const generatedNumber = Math.floor(Math.random() * 100 + 1)

    const encodedCallback = contractInstance.methods.__callback(sequence, generatedNumber).encodeABI()
    const tx = {
        from: myAddress,
        gas: 6e6,
        gasPrice: 5,
        to: contractAddress,
        data: encodedCallback,
        chainId: 3
    }

    web3.eth.accounts.signTransaction(tx, privateKey).then(signed => {
        console.log('Generating transaction...')
        web3.eth.sendSignedTransaction(signed.rawTransaction)
            .on('receipt', result => {
                console.log('Callback transaction confirmed!')
            })
            .catch(error => console.log(error))
    })
}
  1. 记得在文件加载完毕后运行start函数来启动 oracle:
start()
  1. 可选地,我们可以添加一个函数来执行来自我们智能合约的generateRandom()函数,以验证我们是否确实收到了另一个订阅的事件,例如以下内容:
// To send a transaction to run the generateRandom function
function generateRandom() {
    const encodedGenerateRandom = contractInstance.methods.generateRandom().encodeABI()
    const tx = {
        from: myAddress,
        gas: 6e6,
        gasPrice: 5,
        to: contractAddress,
        data: encodedGenerateRandom,
        chainId: 3
    }

    web3.eth.accounts.signTransaction(tx, privateKey).then(signed => {
        console.log('Generating transaction...')
        web3.eth.sendSignedTransaction(signed.rawTransaction)
            .on('receipt', result => {
                console.log('Generate random transaction confirmed!')
            })
            .catch(error => console.log(error))
    })
}
  1. 然后,通过使用generateRandom()函数更新start函数以监听我们创建的新事件:
// Setup web3 and start listening to events
function start() {
    const mnemonic = fs.readFileSync(".secret").toString().trim()
    generateAddressesFromSeed(mnemonic)

    // Note that we use the WebsocketProvider because the previous HttpProvider is outdated and doesn't allow subscriptions
    web3 = new Web3(new Web3.providers.WebsocketProvider(infura))
    const ABI = JSON.parse(fs.readFileSync(path.join(__dirname, 'build', 'contracts', 'Oracle.json')))
    contractAddress = ABI.networks['3'].address
    contractInstance = new web3.eth.Contract(ABI.abi, contractAddress)

    console.log('Listening to events...')
    // Listen to the generate random event for executing the __callback() function
    const subscription = contractInstance.events.GenerateRandom()
    subscription.on('data', newEvent => {
        callback(newEvent.returnValues.sequence)
    })

    // Listen to the ShowRandomNumber() event that gets emitted after the callback
 const subscription2 = contractInstance.events.ShowRandomNumber()
 subscription2.on('data', newEvent => {
 console.log('Received random number! Sequence:', newEvent.returnValues.sequence, 'Randomly generated number:', newEvent.returnValues.number)
 })
}

这样你就能够看到合约实际上是如何从你的 Node.js oracle 接收到你的随机生成数字以确认它是否正常工作的。尝试自己部署自己的 Oracle,以通过唯一标识符的基于回调的机制为智能合约提供外部数据,而它们自己无法获取。此外,您可以添加一些外部证明来验证数据是否来自正确的 Oracle,尽管这超出了本指南的范围,因为描述起来太复杂了。

像往常一样,如果你想要查看最新的更改并尝试工作版本,你可以在我的 GitHub 上找到完整的、更新的代码(github.com/merlox/oracle)。如果你想要查看我是如何设置它的,可以看一下 Truffle 配置文件。

改进你的开发工作流程

当涉及到创建智能合约和去中心化应用时,一个常见的问题是我们必须以最有效的方式工作,以创建最高质量的代码,这样我们就不会花费不必要的时间去解决一开始就不应该存在的问题。

根据我的个人经验,我创建的最好的应用程序都是通过事先进行详尽的规划而诞生的。它可能感觉是不必要的,但你工作得越多,你就越意识到通过清晰描述你的想法的详细计划来节省多少时间。

你是否曾经在项目中遇到过不断出现问题,如 bug 或混乱?那很可能是因为你没有做足够的规划。在本节中,你将学习如何规划你的应用程序,以建立易于理解的项目,从而使你能够更有效地开发。

假设你想要将你的技能付诸实践,以了解更多关于以太坊技术的真实项目。所以,你决定在一个相对复杂的 dApp 上工作。你首先得到了这个想法,然后基于你认为它应该如何工作,详细描述了你应用程序的组件,并立即开始编码以快速完成。

对于大多数项目来说,这是一种非常常见的方法,因为我们不想浪费时间在规划上——我们想要立即完成代码的开发。对于小型项目来说这没问题,但对于更大的项目,我们必须遵循以下准则:

  1. 详细描述你心中所想的内容:最重要的特性,对客户的感觉,以及这个项目的主旨。

  2. 将其分解成较小的部分:前端、后端和智能合约(如果有)。然后以一种你能理解它们将如何被使用的方式描述这些元素。

  3. 通过写下将添加到这三个部分的函数来深入了解。这些将是你应用程序的主要函数。在一个空文件中写下它们,没有实际的代码,只有带有参数和返回值的函数。

  4. 使用 NatSpec 文档记录这些函数,以清楚地解释每个参数和返回值在技术层面上应该做什么。

  5. 开始处理较小的独立函数。这些可以是返回某个变量的 getter 函数,或者是用于计算值的简单函数。

  6. 移动到更复杂的函数,直到所有函数都完成为止。在这样做的同时,根据想法编写空测试,以检查这些函数的每个方面。

  7. 通过编写从之前设置的单元测试中得出的单元测试,并添加一些关注它们可能造成的问题潜力的更多单元测试,来校正项目。

您的计划可能会有所不同:你刚刚读到的只是我试图理解成功项目背后的过程后提出的一个简单指南。只是为了让你更直观地理解,这里有一个该开发过程的示意图:

当您创建每个函数时,请留下//TODO注释,描述接下来需要做什么,这样当您回来时,您就有一个清晰简明的目标可供实现。例如,这是我最近正在努力的一个函数:

constructor(address _identityRegistryAddress, address _tokenAddress) public {
    require(_identityRegistryAddress != address(0), 'The identity registry address is required');
    require(_tokenAddress != address(0), 'You must setup the token rinkeby address');
    hydroToken = HydroTokenTestnetInterface(_tokenAddress);
    identityRegistry = IdentityRegistryInterface(_identityRegistryAddress);
    // TODO Uncomment this when the contract is completed
    /* oraclize_setProof(proofType_Ledger); */
}

著名的 atom.io 代码编辑器中已经安装了一个名为language-todo的扩展,它会突出显示这些类型的TODO注释,以便您可以轻松看到它们。您还可以使用搜索功能在整个项目中查找这些注释。

此外,还有另一个扩展,允许您在一个单独的面板中管理这些提醒。以下是包名称,如果您愿意,可以安装它:

以下是一些额外的建议,以提高您在创建成功项目时的工作流程:

  • 在每个文件的顶部,创建一个列出了需要在该文件中完成的事项的列表,这样你就知道何时完成了它。

  • 使用已有的工具部署您的合约并编写测试以有效验证功能。例如,Truffle、Ganache 和 Remix 在测试和提高效率方面是必不可少的。

  • 为每件事情设定一个时间限制;尽量精确,因为项目往往会耗尽你给予的所有时间。要严格保持专注。

  • 确定在不同时间限制下可以完成哪些工作。例如,在 1 周内,您可以使用核心两个功能创建您的想法的基本版本,在 1 天内,您可以完成所需的 100 个功能中的 5 个,以完成一个稳定的 beta 版本,而在 1 个月内,您可以完成基本代码。关键是想象在足够时间内完成您的想法的可能性的现实估计。记录下需要在 1 天内、1 周内和 1 个月内完成的工作。

  • 将自己放在你感到舒适的地方。通常,当你的身体感觉良好,心情放松时,会有伟大的想法涌现。例如,一个水温合适,水流不断的淋浴是检查你的假设和探索可能很棒的新想法的最佳场所之一。

  • 始终记得为你的项目创建一个 Git 存储库,即使你认为你不会用它,因为往往你会需要几年前做的某个特定事情的代码片段,现在你需要为新项目记起。将你的代码保存在 GitHub 上也很好,可以看到你作为开发人员的进步,并建立一个坚实的在线存在。

想出好点子可能需要单独一章。创造力的问题在于只有当你打破常规时才能获得它,因为你不能指望你的头脑基于同样的日常经验创造新的联想。去新的本地地方旅行,探索奇怪的爱好,并真正对与你熟悉的完全不同的主题感兴趣,即使它们一开始看起来很无聊。

摘要

你刚刚完成了本书中最重要的章节之一,因为我们讨论了优化和效率,这两个对你所做的每一个项目都至关重要的事情。我们首先构建了更好的 React 应用程序,你学会了如何使用这个强大的框架优化创建前端应用程序的方式,以及如何正确地结构化你的组件的有趣技巧。

然后,你学会了使用 NGINX 创建集中式 Node.js 应用程序,你可以将其用于智能合约不够的混合项目,包括从想法到代码再到在 VPS 服务器上部署带有 HTTPS 证书的过程中的所有步骤。之后,你探索了几个 web3.js 的改进,以创建具有事件订阅、辅助函数和可以更好地控制的 promises 的更强大的前端。

当谈到创建能力强大的智能合约时,你已经经历了最有趣的话题之一:预言机,因为它们为智能合约提供了有价值的外部信息,这些信息对于特定应用可能是不可或缺的。最后,你发现了 14 个改进项目创建思路的提示,这样你就可以在努力提供更高质量代码的过程中变得熟练。

在下一章中,你将开始从头构建一个非常有趣的去中心化交易所,这是一个令人兴奋的机会,你会喜欢的!

第九章:去中心化交易所工作流程

去中心化交易所,也被称为DAXs,是一个热门话题,简单的原因是所有加密货币都需要通过其他货币进行交换,以赋予它们某种实用性。你能想象一个世界,在这个世界里你不能用比特币换取美元吗?或者以太坊换比特币?这将摧毁大多数加密货币的实际实用性。这就是为什么我们有交易所,为了允许各种货币在自由市场中交易。我们将首先介绍关于 DAXs 的解释,以便你理解它们背后的思想。然后你会理解订单是如何进行的以及如何以安全的方式管理用户资金。之后,你将创建一个具有复杂智能合约和详细界面的真实世界 DAX。

在这一章中,我们将涵盖以下主题:

  • 介绍去中心化交易所

  • 理解交易和匹配引擎

  • 管理加密货币钱包和冷存储

  • 构建用户界面

  • 构建以太坊后端

  • 完成 dApp

介绍去中心化交易所

到底什么是 DAXs?嗯,普通的交易所,比如股票市场,建立在一个集中式系统之上,其中一个服务器处理所有订单并向用户显示结果。他们运行非常高效的系统,但是要建立这样的系统成本相当高,尽管考虑到它们提供的效用是可以理解的。另一方面,DAXs 不依赖于一个集中式系统,所有订单都必须通过一个进行必要计算的服务器进行处理。相反,DAXs 基于以太坊的基础架构工作,为用户提供一个可以由任何人执行并由庞大的计算机网络处理的系统。

与中心化交易所相比,DAX 的第一个区别是它们受到背后技术的限制。你不能创建一个交易法币的 DAX,比如美元或欧元,因为这些货币是基于不同的技术的;它们在一个称为 FOREX 的不同市场上运行,那里的全球银行交易全球货币。同样,你也不能在股票市场交易 ERC20 代币,因为它们是基于以太坊运行的,而且在那些中心化交易所工作的开发者没有创建这些系统之间流畅连接所需的工具——主要原因是速度上的差异。

以太坊自然会产生较慢的交易,因为它们必须被网络的每个节点确认。这就是为什么在 DAXs 中预期会有一个慢速交易系统。然而,还有一些扩展技术,比如 plasmastate channels,允许你在初始设置后更有效地进行交易。我们将探讨它们的工作原理,并构建一个 DAX,以便你理解它们是如何工作的。你甚至可以创建自己的规则。

DAXs 的缺点

DAXs 通常较慢,因为除非你依赖于货币对之间的链下系统,否则无法进行即时交易,在你希望在其他加密货币之间进行交易时将会放慢你的速度。

它们在某种程度上也受限,因为你不能交易不同区块链上基于法定货币或加密货币的货币。例如,交换比特币BTC)兑换以太坊ETH)的唯一方法是拥有中心化系统,该系统持有这两种货币,并在任何时刻提供用户公平的交换。有一些项目已经整合了这两种类型的货币,但它们仍然年轻,并需要成熟才能变得受欢迎。

DAXs 目前尚未被主流公众使用,所以它们没有达到它们可能达到的水平,因为我们缺乏创建无故障工作的交易所所需的工具和协议。

DAXs 的优势

另一方面,这些类型的交易所有可能克服多数依赖中心化交易的大多数市场过时技术。因为它们是从零开始创建的,所以它们可以在其他项目中获取所有优点,并将其更好地实施。

DAXs 默认可以使用数千种代币,因为它们大多数实施了 ERC20 标准,为它们提供了巨大的可能性。有许多优秀的项目正在构建协议,比如0xprotocol,其中开发人员可以将一组已知功能添加到自己的系统中,以便它们可以自由通信,作为一个全球互连 DAXs 系统。事实上,0xprotocol 分享了代币的流动性,使它们可以在没有任何要求的情况下作为交易者运行。

随着以太坊核心团队开发的新的扩容解决方案,DAXs 即将大幅改善,交易速度更快,类似于真实的中心化市场,使以太坊成为全球虚拟货币经济中的核心参与者。

许多成功的交易所正不断提高以扩展去中心化技术的可能性范式,并且它们正在使用稳定币,如泰达币和 USD Coin,以维持以法定货币支持的不变价值,从而弥合了两个世界之间的鸿沟。

我们可以在几本不同的书中讨论数小时有关 DAXs,但我想要传达的观点是,DAXs 有潜力超越现有技术,成为全球中心化和去中心化货币市场的主要场所。这就是为什么我希望你通过构建基于 solidity 智能合约的简单 DAX 来明白所有这些是如何可能的,以获取为创建 DAXs 的许多公司工作所需的实践经验,甚至自己开始 DAX,因为它们是去中心化技术的核心要素之一。

基本交易术语

交易世界广阔而复杂;这就是为什么使用它们的人们创造了许多术语来帮助彼此准确理解它们的含义。例如,与其说 我想购买 10 个 BTC,希望未来价格上涨,你可以说 我想多买入 10 个 BTC。它们的意思是一样的,但这是一种更精确的相互沟通方式。

让我们学习一些重要的术语,以理解交易市场的一些方面:

  • 市价单:一种以最低或最高价格买入或卖出货币的行为。你看,交易所有卖家和买家,那些想要摆脱某种货币的人和那些想要获取一些货币的人。每个人都为自己想要得到的东西设定一个价格,价格总是成对出现的。例如,我想以 50 个 ETH 的价格购买 10 个 BTC。在这种情况下,这对将是 BTC-ETH,因为你声明你想用你的比特币交换以太币;在那里,你同时是比特币的买家和以太币的卖家。人们设定不同的价格,所以当你进行市价交易时,你只是以最大的利润买入或卖出。当你用美元在线购买东西时也是一样的。如果你是欧洲人,像我一样,你会注意到许多在线物品的价格是以美元计价的,这使得用欧元购买这些物品成为不可能,因为它们不是同一种货币。那么当你购买书籍时会发生什么?在后台,一些程序以市场设定的价格将欧元兑换为美元,并用美元购买书籍。

  • 限价单:一种你自己设定的固定价格买入或卖出的行为。这些类型的订单用于那些预测价格变动或愿意等待订单在较长时间内得到执行的人。

  • 经纪人:一位向你提供贷款以进行交易活动的人。经纪人通常会帮助你执行交易等行动,因为他们有更多的资金,所以在你所在的交易所享有特权。

  • 保证金账户:一种特殊类型的用户账户,你可以在其中在交易时从经纪人那里借钱。

  • 多买入:一种购买特定货币的行为,因为你相信它将升值以获利,或者支持货币背后的技术。

  • 空头交易:一种当你看空的货币价值下跌时赢得的行为。例如,你可以说,我要做空欧元,因为我相信价格会在接下来的五天内下跌。这是一个你可以卖出你不拥有的货币的系统。其背后的推理包括以下内容:

    • 首先,你从另一个人,称为经纪人,那里借钱,他会给你想要做空的货币的所需数量,比如你做空的 100 个 ETH。

    • 你会自动以市场价格出售那 100 个 ETH。

    • 在稍后的日期,你会购买这 100 个 ETH。这被称为平仓。例如,20 天后,你以市价购买 100 个 ETH 来平掉你的空头仓位。

    • 根据买入和卖出时的价格,你会赚取或亏损资金。如果你在高价位做空,然后在低价位平仓,你会赚取价格差。例如,如果你以每个以太币 20 美元的价格做空 100 个 ETH,5 天后以太币价格为 10 美元时平仓,你将赚取每个以太币 10 美元,总计 100 个 ETH × 10 美元 = 1000 美元。

    • 通常做空只在保证金账户上可用。这些账户是你可以从经纪人借钱的账户,但有一些限制。

还有买价和卖价,分别等同于买入和出售。现在你更好地理解了一些复杂的概念,你可以继续学习我们将在本章构建的 DAX 更多相关内容。

理解交易和匹配引擎

交易和匹配引擎是一组使用不同类型算法创建和关闭订单的函数。算法可能专注于完成具有更高价格或之前执行的订单。这取决于开发人员的偏好。因为我们将使用智能合约和 ERC20 代币,我们的引擎将专注于按照订单依次快速完成,因为将会是用户关闭订单,前端将包含大部分逻辑。

我们无法在智能合约中处理大量信息,因为 gas 费用昂贵,所以我们让 React 应用程序控制交易以保护人们的资金。

让我们从计划我们将需要的函数开始,这样当我们创建合约和前端函数时就有了坚实的基础:

/// @notice To create a limit order for buying the _symbolBuy while selling the _symbolSell.
/// @param _symbolBuy The 3 character or more name of the token to buy.
/// @param _symbolSell The 3 character or more name of the token to sell.
/// @param _priceBid The price for how many _symbolBuy tokens you desire to buy. For instance: buy 10 ETH for 1 BTC.
/// @param _priceAsk The price for how many tokens you desire to sell of the _symbolSell in exchange for the _symbolBuy. For instance: sell 10 BTC for 2 ETH.
function createLimitOrder(bytes32 _symbolBuy, bytes32 _symbolSell, uint256 _priceBid, uint256 _priceAsk) public {}

那个函数签名(带参数但不含主体的函数名称)将负责生成限价订单。让我们看一些示例,并检查函数签名是否正确:

例如,我想用 90 个 ETH 换取 7 个 BTC,执行以下代码:

function createLimitOrder("ETH","BTC", 90, 7);

正如你所见,我们将符号的顺序颠倒,将卖出订单转换为买入订单,用户愿意用 ETH 换取 BTC。它具有相同的效果,只需一个函数,而不需要为出售创建专门函数。

例如,我想用 20 个 ETH 买入 10 个 BTC

function createLimitOrder("BTC", "ETH", 10, 20);

在这种情况下,我们将符号按照期望的顺序放置,因为我们正在创建一个买入 BTC 并卖出 ETH 的限价订单。现在我们可以创建市价订单函数的签名。

市价订单很有趣,因为我们希望以最便宜或最贵的价格立即填充订单。背后发生的是我们用市价订单来关闭限价订单。然而,通常不可能以最新市价填满整个订单,简单的原因是最赚钱的限价订单是购买或出售最少数量的代币。

例如,我们想以市场价格出售 10 个 TokenA 换取 TokenB。最有利可图的限价订单是以 40 个 TokenB 换取 5 个 TokenA。在这种情况下,1 个 TokenA 的价格将为 8 个 TokenB,反之亦然。因此,我们创建了市价订单,立即从该限价订单中出售 5 个 TokenA 以换取 40 个 TokenB,但是我们想要出售的剩余的 5 个 TokenA 怎么办?我们转向下一个最有利可图的买单,即以 700 个 TokenB 换取 100 个 TokenA。在这种情况下,1 个 TokenA 的价格将为 7 个 TokenB,虽然不如上一个订单的利润高,但仍然不错。因此,我们交换了 5 个 TokenA 以换取 35 个 TokenB,将该限价买单保留在以 665 个 TokenB 购买 95 个 TokenA,直到下一个用户填满为止。

最后,我们使用那一特定时刻的最有利可图限价订单的组合,以 10 个 TokenA 获得了 75 个 TokenB。通过这种理解,我们可以创建我们的市场订单功能的签名:

/// @notice The function to create market orders by filling existing limit orders
/// @param _type The type of the market order which can be "Buy" or "Sell"
/// @param _symbol The token that we want to buy or sell
/// @param _maxPrice The maximum price we are willing to sell or buy the token for, set it to 0 to not limit the order
function createMarketOrder(bytes32 _type, bytes32 _symbol, uint256 _maxPrice);

_maxPrice参数只是一个数字,表示您愿意出售的最低价格,或者您愿意购买的最高价格。默认情况下为零,即无限制,因此只要有卖家或买家可用,您就会得到最有利可图的价格。

管理加密货币钱包和冷存储

当涉及到管理人们的资金时,我们必须格外注意我们的操作方式,因为使用我们的 DAX 可能会面临数百万美元的风险。这就是为什么最大的交易所采用冷存储并配备了许多安全系统的原因。基本上,他们会将资金离线存储在远程位置的安全硬件设备中,这些设备根据其需求进行定制,如 Trezor、Ledger 或他们自己的设备。

在我们的情况下,我们将资金存储在一系列智能合约中,称为托管合约,其唯一目标是存储人们的资金。每个用户帐户将关联一个托管合约,独立安全地保管所有他们的资金。该托管合约将具有一个函数来接收资金,仅限 ERC20 代币,并具有一个可以由该托管合约所有者执行的提取资金的函数。现在,请创建一个名为decentralized-exchange的文件夹,然后运行truffle initnpm init -y命令,在contracts/文件夹中创建一个名为Escrow.sol的合约。以下是我们的托管合约的外观。

首先,它包含了 ERC20 代币的接口,因为我们不需要整个实现来进行代币交易:

pragma solidity 0.5.4;

interface IERC20 {
    function transfer(address to, uint256 value) external returns (bool);
    function approve(address spender, uint256 value) external returns (bool);
    function transferFrom(address from, address to, uint256 value) external returns (bool);
    function totalSupply() external view returns (uint256);
    function balanceOf(address who) external view returns (uint256);
    function allowance(address owner, address spender) external view returns (uint256);
    event Transfer(address indexed from, address indexed to, uint256 value);
    event Approval(address indexed owner, address indexed spender, uint256 value);
}

接着,我们添加了Escrow合约,用于管理每个用户的资金:

contract Escrow {
    address public owner;

    modifier onlyOwner {
        require(msg.sender == owner, 'You must be the owner to execute that function');
        _;
    }

    /// @notice This contract does not accept ETH transfers
    function () external { revert(); }

    /// @notice To setup the initial tokens that the user will store when creating the escrow
    /// @param _owner The address that will be the owner of this escrow, must be the owner of the tokens
    constructor (address _owner) public {
        require(_owner != address(0), 'The owner address must be set');
        owner = _owner;
    }

    /// @notice To transfer tokens to another address, usually the buyer or seller of an existing order
    /// @param _token The address of the token to transfer
    /// @param _to The address of the receiver
    /// @param _amount The amount of tokens to transfer
    function transferTokens(address _token, address _to, uint256 _amount) public onlyOwner {
        require(_token != address(0), 'The token address must be set');
        require(_to != address(0), 'The receiver address must be set');
        require(_amount > 0, 'You must specify the amount of tokens to transfer');

        require(IERC20(_token).transfer(_to, _amount), 'The transfer must be successful');
    }

    /// @notice To see how many of a particular token this contract contains
    /// @param _token The address of the token to check
    /// @return uint256 The number of tokens this contract contains
    function checkTokenBalance(address _token) public view returns(uint256) {
        require(_token != address(0), 'The token address must be set');
        return IERC20(_token).balanceOf(address(this));
    }
}

这个Escrow合约接收代币转账以将资金安全地保存在内部。每个用户将拥有一个独特的托管合约来分散资金的位置,以便攻击者无法集中于单一地点。您可以通过transferTokens()函数管理合约内的代币资金,并且可以使用checkTokenBalance()函数检查合约内的代币余额,这是一个简化的.balanceOf()ERC20 辅助函数。最后,我添加了一个空的非付款回退函数,以避免接收 Ether,因为我们只想要代币内部。

我们将稍后使用这个Escrow合约来管理人们的资金,因为我们希望有一个安全的地方来保存他们珍贵的代币。理想情况下,我们会创建一个使用硬件设备中的冷存储的系统,但这样的操作将需要一个复杂的系统,负责安全管理每一个步骤,以防止中间人攻击。

构建用户界面

DAXs 的用户界面与传统交易所(如股票交易所)或中心化加密交易所(如币安)使用的界面相同。其理念是提供一个数据驱动的设计,使他们能够快速了解所选代币对的情况。中心区域将用于数据显示,侧边栏将用于用户可能采取的操作,右侧将用于辅助数据;在我们的情况下,它将用于过去的交易。

像往常一样,创建一个包含我们项目的srcdist文件夹。您可以通过查看我的 GitHub 上的自己版本 github.com/merlox/dapp 来直接复制之前项目的设置。我们的设计将基于大多数交易所,因为它们有一个经过研究的公式,感觉很棒。在您的index.js文件中创建一个新组件作为侧边栏的一部分。

首先,添加Main组件以及普通 React 应用程序所需的导入:

import React from 'react'
import ReactDOM from 'react-dom'
import './index.styl'

class Main extends React.Component {
    constructor() {
        super()
    }

    render() {
        return (
            <div>
                <Sidebar />
            </div>
        )
    }
}

然后添加Sidebar组件,其中包含用户可以执行的一些基本操作,例如资金管理部分以添加或提取资金,以及创建买入或卖出订单的部分:

/// Create the basic sidebar html, then we'll add the style css
// The sidebar where you take all your actions
class Sidebar extends React.Component {
    constructor() {
        super()
        this.state = {
            showLimitOrderInput: false
        }
    }

    render() {
        return (
            <div className="sidebar">
                <div className="selected-assets-title">Selected assets:</div>
                <div className="selected-asset-one">ETH</div>
                <div className="selected-asset-two">BAT</div>
                <div className="your-portfolio">Your portfolio:</div>
                <div className="grid-center">ETH:</div><div className="grid-center">10</div>
                <div className="grid-center">BAT:</div><div className="grid-center">200</div>
                <div className="money-management">Money management:</div>
                <button className="button-outline">Deposit</button>
                <button className="button-outline">Withdraw</button>
                <div className="actions">Actions:</div>
                <button>Buy</button>
                <button className="sell">Sell</button>
                <select defaultValue="market-order" onChange={selected => {
                    if(selected.target.value == 'limit-order') this.setState({showLimitOrderInput: true})
                    else this.setState({showLimitOrderInput: false})
                }}>
                    <option value="market-order">Market Order</option>
                    <option value="limit-order">Limit Order</option>
                </select>
                <input ref="limit-order-amount" className={this.state.showLimitOrderInput ? '' : 'hidden'} type="number" placeholder="Price to buy or sell at..."/>
            </div>
        )
    }
}

您添加的类和元素完全由您决定。我个人喜欢向用户显示他们正在交易的货币对,每个货币对的余额以及一组操作,如购买、出售、存款和提款。然后,我们可以添加一些css。在这个项目中,我们将使用一种称为styluscss预处理器(stylus-lang.com),它允许您在没有括号和嵌套类的情况下编写css,以及许多其他很好的功能。您可以按照以下步骤安装它:

npm i -S style-loader css-loader stylus-loader stylus

然后将其添加到你的webpack配置文件中作为新的规则块:

{
    test: /\.styl$/,
    exclude: /node_modules/,
    use: ['style-loader', 'css-loader', 'stylus-loader']
}

在你的源文件夹内创建一个新的index.styl文件,并添加你的 Stylus 代码。如果你想创建和我一样的设计,请在官方 GitHub 上查看 stylus 代码:github.com/merlox/decentralized-exchange/blob/master/src/index.styl

这为我们的 DAX 生成了一个漂亮的侧边栏。记得用webpack -w -d打包你的文件:

正如你所看到的,Stylus 允许你编写清晰、可嵌套的css,以便轻松组织大块的样式,从而使你的项目更易于维护。最后,该代码会被转换成有效的在所有浏览器上运行的css,因为 Stylus 会正确地编译每个文件。然后我们可以添加一个交易部分,展示在我们的交易所中所有货币对的交易,以便人们了解他们的硬币的总体价格。

首先,在Main组件的状态对象中添加假数据的新交易以实现最终设计时 dApp 的展示:

import React from 'react'
import ReactDOM from 'react-dom'
import './index.styl'

class Main extends React.Component {
    constructor() {
        super()

        this.state = {
            trades: [{
                id: 123,
                type: 'buy',
                firstSymbol: 'ETH',
                secondSymbol: 'BAT',
                quantity: 120, // You want to buy 120 firstSymbol
                price: 200 // When buying, you get 1 firstSymbol for selling 200 secondSymbol
            }, {
                id: 927,
                type: 'sell',
                firstSymbol: 'ETH',
                secondSymbol: 'BAT',
                quantity: 80, // You want to buy 80 secondSymbol
                price: 305 // When selling, you get 305 secondSymbol for selling 1 firstSymbol
            }],
            history: [{
                id: 927,
                type: 'buy',
                firstSymbol: 'ETH',
                secondSymbol: 'BAT',
                quantity: 2,
                price: 20
            }, {
                id: 927,
                type: 'sell',
                firstSymbol: 'ETH',
                secondSymbol: 'BAT',
                quantity: 2, // You want to buy 80 secondSymbol
                price: 10 // When selling, you get 305 secondSymbol for selling 1 firstSymbol
            }]
        }
    }

之后,通过将 props 传递给TradesHistory组件,用新的状态对象更新render()函数:


    render() {
        return (
            <div className="main-container">
                <Sidebar />
                <Trades
                    trades={this.state.trades}
                />
                <History
                    history={this.state.history}
                />
            </div>
        )
    }
}

创建新的Trades组件,以便显示我们刚刚添加的交易:

// The main section to see live trades taking place
class Trades extends React.Component {
    constructor() {
        super()
    }

    render() {
        let buyTrades = this.props.trades.filter(trade => trade.type == 'buy')
        buyTrades = buyTrades.map((trade, index) => (
            <div key={trade.id + index} className="trade-container buy-trade">
                <div className="trade-symbol">{trade.firstSymbol}</div>
                <div className="trade-symbol">{trade.secondSymbol}</div>
                <div className="trade-pricing">{trade.type} {trade.quantity} {trade.firstSymbol} at {trade.price} {trade.secondSymbol} each</div>
            </div>
        ))
        let sellTrades = this.props.trades.filter(trade => trade.type == 'sell')
        sellTrades = sellTrades.map((trade, index) => (
            <div key={trade.id + index} className="trade-container sell-trade">
                <div className="trade-symbol">{trade.firstSymbol}</div>
                <div className="trade-symbol">{trade.secondSymbol}</div>
                <div className="trade-pricing">{trade.type} {trade.quantity} {trade.firstSymbol} at {trade.price} {trade.secondSymbol} each</div>
            </div>
        ))
        return (
            <div className="trades">
                <div className="buy-trades-title heading">Buy</div>
                <div className="buy-trades-container">{buyTrades}</div>
                <div className="sell-trades-title heading">Sell</div>
                <div className="sell-trades-container">{sellTrades}</div>
            </div>
        )
    }
}

正如你所看到的,由于我们需要它们来了解我们的交易所在实际环境中的样子,我们增加了许多样例交易和历史交易;请注意我们如何更新了Main组件,以将状态数据传递给每个组件。然后我们可以添加一些 Stylus 让它看起来更好。在官方 GitHub 上查看最终的 Stylus 代码:github.com/merlox/decentralized-exchange/blob/master/src/index.styl

为了得到一个外观漂亮的设计。请注意,我在Main组件的状态对象中包含了 15 个交易对象和 15 个历史交易对象,以便我们在完全加载后看到 dApp 的样子:

每个 BUY 和 SELL 部分顶部最上面的交易是该加密货币对的市价,因为市价订单在那个特定时刻一直是最有利可图的交易。随着人们随着时间交易不同的货币,这些交易将实时更新。这是了解价格走势的一种奇妙方式。最后,我们可以添加History部分,它将显示最近的交易:

// Past historical trades
class History extends React.Component {
    constructor() {
        super()
    }

    render() {
        const historicalTrades = this.props.history.map((trade, index) => (
            <div key={trade.id + index} className="historical-trade">
                <div className={trade.type == 'sell' ? 'sell-trade' : 'buy-trade'}>{trade.type} {trade.quantity} {trade.firstSymbol} for {trade.quantity * trade.price} {trade.secondSymbol} at {trade.price} each</div>
            </div>
        ))
        return (
            <div className="history">
                <div className="heading">Recent history</div>
                <div className="historical-trades-container">{historicalTrades}</div>
            </div>
        )
    }
}

ReactDOM.render(<Main />, document.querySelector('#root'))

请记得添加来自react-dom包的render()函数以渲染你的组件。然后我们可以添加更多css

.history
    padding: 15px
    background-color: whitesmoke
    height: 100vh
    overflow: auto

    .historical-trades-container
        text-align: center

        .historical-trade
            font-size: 0.95em
            margin-bottom: 10px

            &:first-letter
                text-transform: uppercase

            .sell-trade
                color: rgb(223, 98, 98)

            .buy-trade
                color: rgb(98, 133, 223)

现在,如果你运行webpackhttp-server,你会看到我们的成品。由于我们的目标是创建一个用于桌面电脑的交易所,而要验证每个断点以适应手机和平板电脑的不同尺寸是一项耗时的任务,因此它对移动设备不具有响应性:

这将是我们的基本设计。您可以自由添加更多的货币对,使用 ERC20 代币,使用 D3.js 创建图表,甚至使用状态通道!本书中展示的项目的优点在于,您可以在现有结构的基础上构建一个真正高质量的产品,该产品可用于 ICO 或使用您自己的解决方案来扩展 dApps 生态系统。让我们继续构建所需的智能合约来创建交易并使用 MetaMask 进行交易。

构建以太坊后端

我们项目的后端将负责生成可以由任何人填写的交易,只要他们有足够的资金支付已确定的价格即可。当用户注册时,他们将部署一个 Escrow 合约,该合约将由我们的主要 DAX 合约使用。因此,让我们首先设置要求和合约结构,然后开始填写所有功能以练习在 第四章 中学习的系统以提高开发者的效率,精通智能合约

首先,在文件开头的大型注释中定义我们将需要的函数:

// Functions that we need:
/*
    1\. Constructor to setup the owner
    2\. Fallback non-payable function to reject ETH from direct transfers since we only want people to use the functions designed to trade a specific pair
    3\. Function to extract tokens from this contract in case someone mistakenly sends ERC20 to the wrong function
    4\. Function to create whitelist a token by the owner
    5\. Function to create market orders
    6\. Function to create limit orders
 */

设置所使用的 Solidity 版本,导入 Escrow 合约,并定义令牌接口:

pragma solidity ⁰.5.4;

import './Escrow.sol';

interface IERC20 {
    function transfer(address to, uint256 value) external returns (bool);
    function approve(address spender, uint256 value) external returns (bool);
    function transferFrom(address from, address to, uint256 value) external returns (bool);
    function totalSupply() external view returns (uint256);
    function balanceOf(address who) external view returns (uint256);
    function allowance(address owner, address spender) external view returns (uint256);
    event Transfer(address indexed from, address indexed to, uint256 value);
    event Approval(address indexed owner, address indexed spender, uint256 value);
}

首先通过定义我们将用于创建新订单的 Order 结构来创建 DAX 合约:

contract DAX {
    event TransferOrder(bytes32 _type, address indexed from, address indexed to, bytes32 tokenSymbol, uint256 quantity);
    enum OrderState {OPEN, CLOSED}

    struct Order {
        uint256 id;
        address owner;
        bytes32 orderType;
        bytes32 firstSymbol;
        bytes32 secondSymbol;
        uint256 quantity;
        uint256 price;
        uint256 timestamp;
        OrderState state;
    }

然后定义管理卖单和买单所需的许多变量,同时也将令牌列入白名单:

    Order[] public buyOrders;
    Order[] public sellOrders;
    Order[] public closedOrders;
    uint256 public orderIdCounter;
    address public owner;
    address[] public whitelistedTokens;
    bytes32[] public whitelistedTokenSymbols;
    address[] public users;

创建所需的映射以添加和管理令牌符号,并根据给定的 ID 查找订单:

    // Token address => isWhitelisted or not
    mapping(address => bool) public isTokenWhitelisted;
    mapping(bytes32 => bool) public isTokenSymbolWhitelisted;
    mapping(bytes32 => bytes32[]) public tokenPairs; // A token symbol pair made of 'FIRST' => 'SECOND'
    mapping(bytes32 => address) public tokenAddressBySymbol; // Symbol => address of the token
    mapping(uint256 => Order) public orderById; // Id => trade object
    mapping(uint256 => uint256) public buyOrderIndexById; // Id => index inside the buyOrders array
    mapping(uint256 => uint256) public sellOrderIndexById; // Id => index inside the sellOrders array
    mapping(address => address) public escrowByUserAddress; // User address => escrow contract address

然后,添加 onlyOwner 修饰符,回退函数将还原,以及构造函数:

    modifier onlyOwner {
        require(msg.sender == owner, 'The sender must be the owner for this function');
        _;
    }

    /// @notice Users should not send ether to this contract
    function () external {
        revert();
    }

    constructor () public {
        owner = msg.sender;
    }

使用完整的 NatSpec 文档和函数签名定义白名单令牌函数。我已经突出显示了该函数,以便您可以清楚地区分函数和注释:

     /// @notice To whitelist a token so that is tradable in the exchange
     /// @dev If the transaction reverts, it could be because of the quantity of token pairs, try reducing the number and breaking the transaction into several pieces
     /// @param _symbol The symbol of the token
     /// @param _token The token to whitelist, for instance 'TOK'
    /// @param _tokenPairSymbols The token pairs to whitelist for this new token, for instance: ['BAT', 'HYDRO'] which will be converted to ['TOK', 'BAT'] and ['TOK', 'HYDRO']
    /// @param _tokenPairAddresses The token pair addresses to whitelist for this new token, for instance: ['0x213...', '0x927...', '0x128...']
    function whitelistToken(bytes32 _symbol, address _token, bytes32[] memory _tokenPairSymbols, address[] memory _tokenPairAddresses) public onlyOwner {}

要管理令牌,请创建以下两个带有文档的函数:

    /// @notice To store tokens inside the escrow contract associated with the user accounts as long as the users made an approval beforehand
    /// @dev It will revert is the user doesn't approve tokens beforehand to this contract
    /// @param _token The token address
    /// @param _amount The quantity to deposit to the escrow contract
    function depositTokens(address _token, uint256 _amount) public {}

    /// @notice To extract tokens
    /// @param _token The token address to extract
    /// @param _amount The amount of tokens to transfer
    function extractTokens(address _token, uint256 _amount) public {}

使用它们正常工作所需的参数添加市场和限价订单功能,因为这些将是创建订单和与 DAX 交互的主要功能:

    /// @notice To create a market order by filling one or more existing limit orders at the most profitable price given a token pair, type of order (buy or sell) and the amount of tokens to trade, the _quantity is how many _firstSymbol tokens you want to buy if it's a buy order or how many _firstSymbol tokens you want to sell at market price
    /// @param _type The type of order either 'buy' or 'sell'
    /// @param _firstSymbol The first token to buy or sell
    /// @param _secondSymbol The second token to create a pair
    /// @param _quantity The amount of tokens to sell or buy
    function marketOrder(bytes32 _type, bytes32 _firstSymbol, bytes32 _secondSymbol, uint256 _quantity) public {}

    /// @notice To create a market order given a token pair, type of order, amount of tokens to trade and the price per token. If the type is buy, the price will determine how many _secondSymbol tokens you are willing to pay for each _firstSymbol up until your _quantity or better if there are more profitable prices. If the type if sell, the price will determine how many _secondSymbol tokens you get for each _firstSymbol
    /// @param _type The type of order either 'buy' or 'sell'
    /// @param _firstSymbol The first symbol to deal with
    /// @param _secondSymbol The second symbol that you want to deal
    /// @param _quantity How many tokens you want to deal, these are _firstSymbol tokens
    /// @param _pricePerToken How many tokens you get or pay for your other symbol, the total quantity is _pricePerToken * _quantity
    function limitOrder(bytes32 _type, bytes32 _firstSymbol, bytes32 _secondSymbol, uint256 _quantity, uint256 _pricePerToken) public {}

最后,添加 view 函数,您可以将其用作界面可能需要的帮助程序和获取器。尝试自己添加它们。然后检查解决方案:

    /// @notice Sorts the selected array of Orders by price from lower to higher if it's a buy order or from highest to lowest if it's a sell order
    /// @param _type The type of order either 'sell' or 'buy'
    /// @return uint256[] Returns the sorted ids
 function sortIdsByPrices(bytes32 _type) public view returns (uint256[] memory) {}

    /// @notice Checks if a pair is valid
    /// @param _firstSymbol The first symbol of the pair
    /// @param _secondSymbol The second symbol of the pair
    /// @returns bool If the pair is valid or not
 function checkValidPair(bytes32 _firstSymbol, bytes32 _secondSymbol) public view returns(bool) {}

    /// @notice Returns the token pairs
    /// @param _token To get the array of token pair for that selected token
    /// @returns bytes32[] An array containing the pairs
 function getTokenPairs(bytes32 _token) public view returns(bytes32[] memory) {}
}

首先,我们设置一个 event 来记录令牌转移,以便人们可以看到什么时候卖出或购买了令牌。我们可以添加更多事件,但我将让您自行发现您需要哪些事件。然后,我们添加了大量必要的变量,从一个定义订单是开放还是关闭的 enum 开始。我们为每个订单的每个属性添加了一个 struct,以清晰地定义正在处理哪个令牌。

然后,我们添加了三个数组来存储现有订单,同时还有一些变量来将新代币列入白名单,以便我们可以与更广泛的加密货币集进行交易。之后,我们添加了多个映射以轻松找到每个特定订单,同时优化了 gas 成本。

我们添加了一个onlyOwner修饰符来限制对 whitelisting 函数的访问,以防止在添加加密货币时变得太疯狂。我们添加了一个不允许以太币转账的 fallback 函数,以防止人们向这个交易所发送资金,并添加了一个定义 DAX 所有者的构造函数。

然后,我们添加了whitelistToken()函数,该函数接受一个令牌地址和一个符号数组,用于创建与该主令牌的交易对;这样,你就能够一次交易大量的交易对。depositTokens()函数是由想增加其令牌余额的用户使用的。他们可以直接将他们想要交易的令牌转移到与他们相关联的 Escrow 合约中,但用户必须首先通过这个函数创建一个新的 Escrow,这只能通过这个函数完成。然后,Escrow 地址将与escrowByUserAddress映射中的该帐户关联起来。此存款函数还要求用户之前使用approve()函数来允许 DAX 合约将令牌转移到 Escrow 合约;否则,它将失败。

接下来,extractTokens()函数用于将令牌从托管账户移动到用户的地址。这是一个快捷方式,用于在Escrow合约内部调用transferTokens()函数以便于令牌管理。之后,我们有复杂的市场和限价订单函数。它们都是大函数,因为它们需要对订单进行排序、更新和查找以匹配现有订单,并在区块链的燃气使用限制内工作。我们很快会深入了解它们是如何实现的。最后,我们有一些辅助函数用于对订单进行排序,检查给定的令牌对是否存在,以及检索令牌对的数组。

让我们继续实现一些这些函数。记得从最简单的函数开始,逐步进展到更复杂的函数,这样你就会有一个坚实的结构支撑它们。这是whitelisting函数应该的样子:

/// @notice To whitelist a token so that is tradable in the exchange
/// @dev If the transaction reverts, it could be because of the quantity of token pairs, try reducing the number and breaking the transaction into several pieces
/// @param _symbol The symbol of the token
/// @param _token The token to whitelist, for instance 'TOK'
/// @param _tokenPairSymbols The token pairs to whitelist for this new token, for instance: ['BAT', 'HYDRO'] which will be converted to ['TOK', 'BAT'] and ['TOK', 'HYDRO']
/// @param _tokenPairAddresses The token pair addresses to whitelist for this new token, for instance: ['0x213...', '0x927...', '0x128...']
function whitelistToken(bytes32 _symbol, address _token, bytes32[] memory _tokenPairSymbols, address[] memory _tokenPairAddresses) public onlyOwner {
    require(_token != address(0), 'You must specify the token address to whitelist');
    require(IERC20(_token).totalSupply() > 0, 'The token address specified is not a valid ERC20 token');
    require(_tokenPairAddresses.length == _tokenPairSymbols.length, 'You must send the same number of addresses and symbols');

    isTokenWhitelisted[_token] = true;
    isTokenSymbolWhitelisted[_symbol] = true;
    whitelistedTokens.push(_token);
    whitelistedTokenSymbols.push(_symbol);
    tokenAddressBySymbol[_symbol] = _token;
    tokenPairs[_symbol] = _tokenPairSymbols;

    for(uint256 i = 0; i < _tokenPairAddresses.length; i++) {
        address currentAddress = _tokenPairAddresses[i];
        bytes32 currentSymbol = _tokenPairSymbols[i];
        tokenPairs[currentSymbol].push(_symbol);
        if(!isTokenWhitelisted[currentAddress]) {
            isTokenWhitelisted[currentAddress] = true;
            isTokenSymbolWhitelisted[currentSymbol] = true;
            whitelistedTokens.push(currentAddress);
            whitelistedTokenSymbols.push(currentSymbol);
            tokenAddressBySymbol[currentSymbol] = currentAddress;
        }
    }
}

whitelisting 函数进行一些要求检查,然后为每个给定的令牌对进行白名单设置,以便您可以独立交易。例如,如果你的主要令牌符号是 BAT,并且你的_tokenPairSymbols数组包含['TOK', 'TIK'],你就可以与 BAT - TOK 和 BAT - TIK 交易。简单的事情。只要你保持低数量的令牌对,该函数就不会耗尽燃气。

以下是用于管理令牌资金的下一个函数:

/// @notice To store tokens inside the escrow contract associated with the user accounts as long as the users made an approval beforehand
/// @dev It will revert is the user doesn't approve tokens beforehand to this contract
/// @param _token The token address
/// @param _amount The quantity to deposit to the escrow contract
function depositTokens(address _token, uint256 _amount) public {
    require(isTokenWhitelisted[_token], 'The token to deposit must be whitelisted');
    require(_token != address(0), 'You must specify the token address');
    require(_amount > 0, 'You must send some tokens with this deposit function');
    require(IERC20(_token).allowance(msg.sender, address(this)) >= _amount, 'You must approve() the quantity of tokens that you want to deposit first');
    if(escrowByUserAddress[msg.sender] == address(0)) {
        Escrow newEscrow = new Escrow(address(this));
        escrowByUserAddress[msg.sender] = address(newEscrow);
        users.push(msg.sender);
    }
    IERC20(_token).transferFrom(msg.sender, escrowByUserAddress[msg.sender], _amount);
}

/// @notice To extract tokens
/// @param _token The token address to extract
/// @param _amount The amount of tokens to transfer
function extractTokens(address _token, uint256 _amount) public {
    require(_token != address(0), 'You must specify the token address');
    require(_amount > 0, 'You must send some tokens with this deposit function');
    Escrow(escrowByUserAddress[msg.sender]).transferTokens(_token, msg.sender, _amount);
}

存款函数检查用户是否有与他们地址相关联的Escrow合约。如果没有,函数会创建一个新的Escrow,然后转移用户请求的令牌存款,只要他们之前在适当的 ERC20 合约中批准了一些令牌。

extract 函数只是简单地运行transferTokens()函数到所有者的地址,只要他们之前有一些余额。否则它会回滚。

让我们继续进行限价订单功能。因为这是一个较大的功能,我们将其拆分为更小的部分,以便您了解每个部分的操作方式。

首先,我们根据创建函数时出现的更改更新的文档。改进文档永远不会太迟:

/// @notice To create a market order given a token pair, type of order, amount of tokens to trade and the price per token. If the type is buy, the price will determine how many _secondSymbol tokens you are willing to pay for each _firstSymbol up until your _quantity or better if there are more profitable prices. If the type if sell, the price will determine how many _secondSymbol tokens you get for each _firstSymbol
/// @param _type The type of order either 'buy' or 'sell'
/// @param _firstSymbol The first symbol to deal with
/// @param _secondSymbol The second symbol that you want to deal
/// @param _quantity How many tokens you want to deal, these are _firstSymbol tokens
/// @param _pricePerToken How many tokens you get or pay for your other symbol, the total quantity is _pricePerToken * _quantity

然后,我们运行许多require()检查,以确保用户正确执行限价订单功能:

function limitOrder(bytes32 _type, bytes32 _firstSymbol, bytes32 _secondSymbol, uint256 _quantity, uint256 _pricePerToken) public {
    address userEscrow = escrowByUserAddress[msg.sender];
    address firstSymbolAddress = tokenAddressBySymbol[_firstSymbol];
    address secondSymbolAddress = tokenAddressBySymbol[_secondSymbol];

    require(firstSymbolAddress != address(0), 'The first symbol has not been whitelisted');
    require(secondSymbolAddress != address(0), 'The second symbol has not been whitelisted');
    require(isTokenSymbolWhitelisted[_firstSymbol], 'The first symbol must be whitelisted to trade with it');
    require(isTokenSymbolWhitelisted[_secondSymbol], 'The second symbol must be whitelisted to trade with it');
    require(userEscrow != address(0), 'You must deposit some tokens before creating orders, use depositToken()');
    require(checkValidPair(_firstSymbol, _secondSymbol), 'The pair must be a valid pair');

之后,如果用户创建买入限价订单,则执行buy功能:

    Order memory myOrder = Order(orderIdCounter, msg.sender, _type, _firstSymbol, _secondSymbol, _quantity, _pricePerToken, now, OrderState.OPEN);
    orderById[orderIdCounter] = myOrder;
    if(_type == 'buy') {
        // Check that the user has enough of the second symbol if he wants to buy the first symbol at that price
        require(IERC20(secondSymbolAddress).balanceOf(userEscrow) >= _quantity, 'You must have enough second token funds in your escrow contract to create this buy order');

        buyOrders.push(myOrder);

        // Sort existing orders by price the most efficient way possible, we could optimize even more by creating a buy array for each token
        uint256[] memory sortedIds = sortIdsByPrices('buy');
        delete buyOrders;
        buyOrders.length = sortedIds.length;
        for(uint256 i = 0; i < sortedIds.length; i++) {
            buyOrders[i] = orderById[sortedIds[i]];
            buyOrderIndexById[sortedIds[i]] = i;
        }

否则,更改订单添加的数组,同时在添加后对订单进行排序:

    } else {
        // Check that the user has enough of the first symbol if he wants to sell it for the second symbol
        require(IERC20(firstSymbolAddress).balanceOf(userEscrow) >= _quantity, 'You must have enough first token funds in your escrow contract to create this sell order');

        // Add the new order
        sellOrders.push(myOrder);

        // Sort existing orders by price the most efficient way possible, we could optimize even more by creating a sell array for each token
        uint256[] memory sortedIds = sortIdsByPrices('sell');
        delete sellOrders; // Reset orders
        sellOrders.length = sortedIds.length;
        for(uint256 i = 0; i < sortedIds.length; i++) {
            sellOrders[i] = orderById[sortedIds[i]];
            sellOrderIndexById[sortedIds[i]] = i;
        }
    }

    orderIdCounter++;
}

这是整个限价订单功能拆分成易于理解的片段,以解释每个语句背后的逻辑。你看到我们使用了sortIdsByPrices函数,因为我们需要对订单数组进行排序。下面是完成后的排序函数的样子。请注意,该函数是view类型,这意味着运行所有计算不会产生任何 Gas 费用,因为它们将在本地执行,因此排序后的数组可以是无限的:

/// @notice Sorts the selected array of Orders by price from lower to higher if it's a buy order or from highest to lowest if it's a sell order
/// @param _type The type of order either 'sell' or 'buy'
/// @return uint256[] Returns the sorted ids
function sortIdsByPrices(bytes32 _type) public view returns (uint256[] memory) {
    Order[] memory orders;
    if(_type == 'sell') orders = sellOrders;
    else orders = buyOrders;

    uint256 length = orders.length;
    uint256[] memory orderedIds = new uint256[](length);
    uint256 lastId = 0;
    for(uint i = 0; i < length; i++) {
        if(orders[i].quantity > 0) {
            for(uint j = i+1; j < length; j++) {
                // If it's a buy order, sort from lowest to highest since we want the lowest prices first
                if(_type == 'buy' && orders[i].price > orders[j].price) {
                    Order memory temporaryOrder = orders[i];
                    orders[i] = orders[j];
                    orders[j] = temporaryOrder;
                }
                // If it's a sell order, sort from highest to lowest since we want the highest sell prices first
                if(_type == 'sell' && orders[i].price < orders[j].price) {
                    Order memory temporaryOrder = orders[i];
                    orders[i] = orders[j];
                    orders[j] = temporaryOrder;
                }
            }
            orderedIds[lastId] = orders[i].id;
            lastId++;
        }
    }
    return orderedIds;
}

注意sortIdsByPrice()函数。它读取包含订单结构的相应状态变量,然后按升序排列订单(如果是买入限价单),或按降序排列订单(如果是卖出限价单)。我们需要它用于限价订单功能。

limitOrder()函数首先检查参数是否有效,以及代币是否可以交易。根据请求的订单类型,它将一个新的Order结构实例推送到sellOrders()buyOrders()数组中,同时对这些数组进行排序,以将这个新的限价订单推送到正确的位置。请记住,我们的想法是有一个排序后的订单数组,以便我们可以快速找到最有利可图的订单;这就是为什么我们有排序功能。最后,它更新订单数组和订单索引映射,以便我们以后可以找到每个Order实例在这些数组中的位置。

现在,我们可以看一下庞大的marketOrder函数实现;这是我自己的方式来实现,我建议你尝试从头开始重新创建一个市场订单功能,考虑到所有的 Gas 限制和限制。虽然不完美,但它清楚地显示了 DAX 交易所的工作原理。以下是您理解的功能分解。首先,更新函数的文档以确保它解释了内部执行的内容:

/// @notice To create a market order by filling one or more existing limit orders at the most profitable price given a token pair, type of order (buy or sell) and the amount of tokens to trade, the _quantity is how many _firstSymbol tokens you want to buy if it's a buy order or how many _firstSymbol tokens you want to sell at market price
/// @param _type The type of order either 'buy' or 'sell'
/// @param _firstSymbol The first token to buy or sell
/// @param _secondSymbol The second token to create a pair
/// @param _quantity The amount of tokens to sell or buy

然后添加require()函数检查以验证给定的代币是否有效,以及数量是否正确:

function marketOrder(bytes32 _type, bytes32 _firstSymbol, bytes32 _secondSymbol, uint256 _quantity) public {
    require(_type.length > 0, 'You must specify the type');
    require(isTokenSymbolWhitelisted[_firstSymbol], 'The first symbol must be whitelisted');
    require(isTokenSymbolWhitelisted[_secondSymbol], 'The second symbol must be whitelisted');
    require(_quantity > 0, 'You must specify the quantity to buy or sell');
    require(checkValidPair(_firstSymbol, _secondSymbol), 'The pair must be a valid pair');

就像限价订单功能一样,根据现有订单的状态执行买入或卖出功能:

    // Fills the latest market orders up until the _quantity is reached
    uint256[] memory ordersToFillIds;
    uint256[] memory quantitiesToFillPerOrder;
    uint256 currentQuantity = 0;
    if(_type == 'buy') {
        ordersToFillIds = new uint256[](sellOrders.length);
        quantitiesToFillPerOrder = new uint256[](sellOrders.length);
        // Loop through all the sell orders until we fill the quantity
        for(uint256 i = 0; i < sellOrders.length; i++) {
            ordersToFillIds[i] = sellOrders[i].id;
            if((currentQuantity + sellOrders[i].quantity) > _quantity) {
                quantitiesToFillPerOrder[i] = _quantity - currentQuantity;
                break;
            }
            currentQuantity += sellOrders[i].quantity;
            quantitiesToFillPerOrder[i] = sellOrders[i].quantity;
        }
    } else {
        ordersToFillIds = new uint256[](buyOrders.length);
        quantitiesToFillPerOrder = new uint256[](buyOrders.length);
        for(uint256 i = 0; i < buyOrders.length; i++) {
            ordersToFillIds[i] = buyOrders[i].id;
            if((currentQuantity + buyOrders[i].quantity) > _quantity) {
                quantitiesToFillPerOrder[i] = _quantity - currentQuantity;
                break;
            }
            currentQuantity += buyOrders[i].quantity;
            quantitiesToFillPerOrder[i] = buyOrders[i].quantity;
        }
    }

当开发如此复杂的逻辑时,添加一些额外的注释永远不会有害。在这里,我添加了一些说明,以提醒自己这个功能应该在更技术层面上如何工作:

    // When the myOrder.type == sell or _type == buy
    // myOrder.owner send quantityToFill[] of _firstSymbol to msg.sender
    // msg.sender send quantityToFill[] * myOwner.price of _secondSymbol to myOrder.owner

    // When the myOrder.type == buy or _type == sell
    // myOrder.owner send quantityToFill[] * myOwner.price of _secondSymbol to msg.sender
    // msg.sender send quantityToFill[] of _firstSymbol to myOrder.owner

现在,我们生成了要填充的订单数组和每个订单所需的数量,我们可以开始使用另一个循环填充每个订单:

    // Close and fill orders
    for(uint256 i = 0; i < ordersToFillIds.length; i++) {
        Order memory myOrder = orderById[ordersToFillIds[i]];

        // If we fill the entire order, mark it as closed
        if(quantitiesToFillPerOrder[i] == myOrder.quantity) {
            myOrder.state = OrderState.CLOSED;
            closedOrders.push(myOrder);
        }
        myOrder.quantity -= quantitiesToFillPerOrder[i];
        orderById[myOrder.id] = myOrder;

我们必须按类型分解,以查看订单实际上是买单还是卖单,以确保我们以正确的数量实现正确的订单:

        if(_type == 'buy') {
            // If the limit order is a buy order, send the firstSymbol to the creator of the limit order which is the buyer
            Escrow(escrowByUserAddress[myOrder.owner]).transferTokens(tokenAddressBySymbol[_firstSymbol], msg.sender, quantitiesToFillPerOrder[i]);
            Escrow(escrowByUserAddress[msg.sender]).transferTokens(tokenAddressBySymbol[_secondSymbol], myOrder.owner, quantitiesToFillPerOrder[i] * myOrder.price);

            sellOrders[sellOrderIndexById[myOrder.id]] = myOrder;

            emit TransferOrder('sell', escrowByUserAddress[myOrder.owner], msg.sender, _firstSymbol, quantitiesToFillPerOrder[i]);
            emit TransferOrder('buy', escrowByUserAddress[msg.sender], myOrder.owner, _secondSymbol, quantitiesToFillPerOrder[i] * myOrder.price);

如果这是一个卖单,我们改变使用的数组,但逻辑是一样的:

        } else {
            // If this is a buy market order or a sell limit order for the opposite, send firstSymbol to the second user
                Escrow(escrowByUserAddress[myOrder.owner]).transferTokens(tokenAddressBySymbol[_secondSymbol], msg.sender, quantitiesToFillPerOrder[i] * myOrder.price);
            Escrow(escrowByUserAddress[msg.sender]).transferTokens(tokenAddressBySymbol[_firstSymbol], myOrder.owner, quantitiesToFillPerOrder[i]);

            buyOrders[buyOrderIndexById[myOrder.id]] = myOrder;

            emit TransferOrder('buy', escrowByUserAddress[myOrder.owner], msg.sender, _secondSymbol, quantitiesToFillPerOrder[i] * myOrder.price);
            emit TransferOrder('sell', escrowByUserAddress[msg.sender], myOrder.owner, _firstSymbol, quantitiesToFillPerOrder[i]);
        }

    }
}

乍一看,你会发现我们不止有三个for循环,这是非常不经优化的,因为它无法处理超过几个订单,但对于不需要中心服务器的 DAX,这是为数不多的解决方案之一。

首先,我们进行所需的检查,以验证用户是否创建了一个具有适当approve()的有效市场订单,以便合同可以自由购买代币。然后,我们开始循环遍历我们排好序的所有订单数组,首先填充利润最高的订单,同时跟踪每个订单将填充多少代币。一旦我们有了要填充的订单列表和数量,我们就可以开始填充其中的每一个。我们应该如何做?

我们更新每个订单的状态,以便在数量为零或减少数量的同时,对完全填充的订单使用enum OrderState.CLOSED。然后我们将正确数量的代币转移到每个用户。这就是buyOrderIndexById[]映射特别有用的地方,因为我们想要更新特定订单而不改变整个数组的顺序,从而节省燃气和处理成本。最后,我们发出一些事件,以指示我们进行了一些代币转移。

这就是全部内容了!以下是完整的合同,以便您了解所有内容是如何联系在一起的。它可以在官方 GitHub 上找到: github.com/merlox/decentralized-exchange/blob/master/contracts/DAX.sol

这是一个相当庞大的合同,所以我建议您为它编写一些测试,以验证它是否正常运行。您可以通过克隆我的 GitHub 并使用这里的所有代码来检查并运行我编写的测试:github.com/merlox/decentralized-exchange

完成 dApp

现在,我们有了一个具有所需逻辑的工作智能合同,我们可以在简单的 React 应用程序中使用 Truffle 和 web3.js 实现 dApp。首先在您的index.js文件中导入所需的组件:

import React from 'react'
import ReactDOM from 'react-dom'
import MyWeb3 from 'web3'
import './index.styl'
import ABI from '../build/contracts/DAX.json'
import TokenABI from '../build/contracts/ERC20.json'

const batToken = '0x850Cbb38828adF8a89d7d799CCf1010Dc238F665'
const watToken = '0x029cc401Ef45B2a2B2D6D2D6677b9F94E26cfF9d'
const dax = ABI.networks['3'].address

在这个原型中,我们将只使用两种代币,以便您学习如何创建应用程序,因为一个具有完整功能的 DAX 超出了本书的范围。我们首先导入所需的 ABIs 来创建代币实例和代币地址。这些就是我们将使用的代币。

首先,通过更新Main组件中的状态对象,添加新的所需变量,我们将使用这些变量与智能合约进行交互。注意,我们移除了交易和历史数组,因为我们将从合约中获取这些数据:

class Main extends React.Component {
    constructor() {
        super()

        this.state = {
            contractInstance: {},
            tokenInstance: {},
            secondTokenInstance: {},
            userAddress: '',
            firstSymbol: 'BAT', // Sample tokens
            secondSymbol: 'WAT', // Sample tokens
            balanceFirstSymbol: 0,
            balanceSecondSymbol: 0,
            escrow: '',
            buyOrders: [],
            sellOrders: [],
            closedOrders: []
        }

        this.setup()
    }

添加bytes32()辅助函数,用于生成 web3.js 所需的有效十六进制字符串:

    // To use bytes32 functions
    bytes32(name) {
        return myWeb3.utils.fromAscii(name)
    }

然后创建setup()函数,初始化 web3.js 实例,并获取用户同意使用他们的 MetaMask 账户凭据:

 async setup() {
        // Create the contract instance
        window.myWeb3 = new MyWeb3(ethereum)
        try {
            await ethereum.enable();
        } catch (error) {
            console.error('You must approve this dApp to interact with it')
        }
        console.log('Setting up contract instances')
        await this.setContractInstances()
        console.log('Setting up orders')
        await this.setOrders()
        console.log('Setting up pairs')
        await this.setPairs()
    }

由于在 react 应用中设置合约更加复杂,我们必须为可维护性创建一个单独的函数:

    async setContractInstances() {
        const userAddress = (await myWeb3.eth.getAccounts())[0]
        if(!userAddress) return console.error('You must unlock metamask to use this dApp on ropsten!')
        await this.setState({userAddress})
        const contractInstance = new myWeb3.eth.Contract(ABI.abi, dax, {
            from: this.state.userAddress,
            gasPrice: 2e9
        })
        const tokenInstance = new myWeb3.eth.Contract(TokenABI.abi, batToken, {
            from: this.state.userAddress,
            gasPrice: 2e9
        })
        const secondTokenInstance = new myWeb3.eth.Contract(TokenABI.abi, watToken, {
            from: this.state.userAddress,
            gasPrice: 2e9
        })
        await this.setState({contractInstance, tokenInstance, secondTokenInstance})
    }

在设置完 web3 和合约实例后,我们可以开始从智能合约获取订单,以便用订单填充我们的用户界面。首先,我们获取用于循环遍历所有订单的数组长度。这是唯一一种安全地考虑到数组中包含的所有元素的方法:

    async setOrders() {
        // First get the length of all the orders so that you can loop through them
        const buyOrdersLength = await this.state.contractInstance.methods.getOrderLength(this.bytes32("buy")).call({ from: this.state.userAddress })
        const sellOrdersLength = await this.state.contractInstance.methods.getOrderLength(this.bytes32('sell')).call({ from: this.state.userAddress })
        const closedOrdersLength = await this.state.contractInstance.methods.getOrderLength(this.bytes32('closed')).call({ from: this.state.userAddress })
        let buyOrders = []
        let sellOrders = []
        let closedOrders = []

然后我们开始循环遍历买单数组,通过独立调用智能合约来处理每个组件:

        for(let i = 0; i < buyOrdersLength; i++) {
            const order = await this.state.contractInstance.methods.getOrder(this.bytes32('buy'), i).call({ from: this.state.userAddress })
            const orderObject = {
                id: order[0],
                owner: order[1],
                type: myWeb3.utils.toUtf8(order[2]),
                firstSymbol: myWeb3.utils.toUtf8(order[3]),
                secondSymbol: myWeb3.utils.toUtf8(order[4]),
                quantity: order[5],
                price: order[6],
                timestamp: order[7],
                state: order[8],
            }
            buyOrders.push(orderObject)
        }

我们对卖单数组也做同样的事情:

        for(let i = 0; i < sellOrdersLength; i++) {
            const order = await this.state.contractInstance.methods.getOrder(this.bytes32('sell'), 0).call({ from: this.state.userAddress })
            const orderObject = {
                id: order[0],
                owner: order[1],
                type: myWeb3.utils.toUtf8(order[2]),
                firstSymbol: myWeb3.utils.toUtf8(order[3]),
                secondSymbol: myWeb3.utils.toUtf8(order[4]),
                quantity: order[5],
                price: order[6],
                timestamp: order[7],
                state: order[8],
            }
            sellOrders.push(orderObject)
        }

再次,我们对关闭订单数组执行相同的操作。我们需要这个数组来显示过去的历史交易,这可以帮助人们了解在加入乐趣之前发生了什么:

        for(let i = 0; i < closedOrdersLength; i++) {
            const order = await this.state.contractInstance.methods.closedOrders(this.bytes32('close'), 0).call({ from: this.state.userAddress })
            const orderObject = {
                id: order[0],
                owner: order[1],
                type: myWeb3.utils.toUtf8(order[2]),
                firstSymbol: myWeb3.utils.toUtf8(order[3]),
                secondSymbol: myWeb3.utils.toUtf8(order[4]),
                quantity: order[5],
                price: order[6],
                timestamp: order[7],
                state: order[8],
            }
            closedOrders.push(orderObject)
        }
        this.setState({buyOrders, sellOrders, closedOrders})
    }

最后,创建一个名为setPairs()的函数,将来用于向平台添加新的令牌对。因为我们不想过度复杂化我们正在创建的初始 DAX,所以我们将自己限制在只有一个令牌对的范围内,由两个虚拟令牌组成,分别命名为WATBAT

    async setPairs() {
        // Here you'd add all the logic to get all the token symbols, in this case we're keeping it simple with one fixed pair
        // If there are no pairs, whitelist a new one automatically if this is the owner of the DAX contract
        const owner = await this.state.contractInstance.methods.owner().call({ from: this.state.userAddress })
        const isWhitelisted = await this.state.contractInstance.methods.isTokenWhitelisted(batToken).call({ from: this.state.userAddress })
        if(owner == this.state.userAddress && !isWhitelisted) {
            await this.state.contractInstance.methods.whitelistToken(this.bytes32('BAT'), batToken, [this.bytes32('WAT')], [watToken]).send({ from: this.state.userAddress, gas: 8e6 })
        }

        // Set the balance of each symbol considering how many tokens you have in escrow
        const escrow = await this.state.contractInstance.methods.escrowByUserAddress(this.state.userAddress).call({ from: this.state.userAddress })
        const balanceOne = await this.state.tokenInstance.methods.balanceOf(escrow).call({ from: this.state.userAddress })
        const balanceTwo = await this.state.secondTokenInstance.methods.balanceOf(escrow).call({ from: this.state.userAddress })
        this.setState({escrow, balanceOne, balanceTwo})
 }
}

我们从设置构造函数开始,其中包含整个应用程序所需的基本变量。然后,setup()函数负责获取所有初始信息。bytes32()函数用于将普通字符串转换为十六进制,因为新版本的 web3 强制我们发送十六进制字符串而不是纯文本以识别bytes32变量。个人而言,我更喜欢将bytes32变量写成字符串,但web3是他们的框架,所以我们必须遵循它的编程风格。然后,我们使用setContractInstances()函数设置合约实例,该函数使用给定的地址和 ABI 启动我们的合约。

然后我们使用setOrders()函数设置订单。这个看起来更吓人,因为它包含了更多的代码,但其实想法很简单,就是从智能合约中获取每个订单,并将它们存储在 react 状态变量的组织良好的数组中。最后,我们使用setPairs()设置令牌对,它会用我们的令牌更新状态。

现在我们需要在智能合约中实现剩余的函数。以下是 React dApp 中白名单函数的样子:

async whitelistTokens(symbol, token, pairSymbols, pairAddresses) {
    await this.state.contractInstance.methods.whitelistToken(this.bytes32(symbol), token, pairSymbols, pairAddresses).send({ from: this.state.userAddress })
}

然后我们实现存款令牌函数,它将增加用户为交易令牌而向平台添加的可用余额。我已经添加了大量注释,让您了解正在发生的事情:

async depositTokens(symbol, amount) {
    if(symbol == 'BAT') {
        // Check the token balance before approving
        const balance = await this.state.tokenInstance.methods.balanceOf(this.state.userAddress).call({ from: this.state.userAddress })
        if(balance < amount) return alert(`You can't deposit ${amount} BAT since you have ${balance} BAT in your account, get more tokens before depositing`)
        // First approve to 0 to avoid errors and then increase it
        await this.state.tokenInstance.methods.approve(dax, 0).send({ from: this.state.userAddress })
        await this.state.tokenInstance.methods.approve(dax, amount).send({ from: this.state.userAddress })
        // Create the transaction
        await this.state.contractInstance.methods.depositTokens(batToken, amount).send({ from: this.state.userAddress })
    } else if(symbol == 'WAT') {
        // Check the token balace before approving
        const balance = await this.state.secondTokenInstance.methods.balanceOf(this.state.userAddress).call({ from: this.state.userAddress })
        if(balance < amount) return alert(`You can't deposit ${amount} WAT since you have ${balance} WAT in your account, get more tokens before depositing`)
        // First approve to 0 to avoid errors and then increase it
        await this.state.secondTokenInstance.methods.approve(dax, 0).send({ from: this.state.userAddress })
        await this.state.secondTokenInstance.methods.approve(dax, amount).send({ from: this.state.userAddress })
        // Create the transaction
        await this.state.contractInstance.methods.depositTokens(watToken, amount).send({ from: this.state.userAddress })
    }
}

提取代币函数相当简单,将用于两种代币:

async withdrawTokens(symbol, amount) {
    if(symbol == 'BAT') {
        await this.state.contractInstance.methods.extractTokens(batToken, amount).send({ from: this.state.userAddress })
    } else if(symbol == 'WAT') {
        await this.state.contractInstance.methods.extractTokens(watToken, amount).send({ from: this.state.userAddress })
    }
}

最后,我们必须实现限价和市价订单功能,讽刺的是,这是最小的功能,因为我们只需要将所需信息传递给智能合约,它就会自行执行整个功能:

async createLimitOrder(type, firstSymbol, secondSymbol, quantity, pricePerToken) {
    // Create the limit order
    await this.state.contractInstance.methods.limitOrder(type, firstSymbol, secondSymbol, quantity, pricePerToken).send({ from: this.state.userAddress })
}

async createMarketOrder(type, firstSymbol, secondSymbol, quantity) {
    // Create the market order
    await this.state.contractInstance.methods.marketOrder(type, firstSymbol, secondSymbol, quantity).send({ from: this.state.userAddress })
}

白名单功能相当简单,因为我们只使用主以太坊地址从智能合约运行白名单功能。请记住,此功能只能由合约的所有者执行。

存款代币函数检查您的以太坊地址中是否有足够的代币,然后创建两个批准:第一个批准是将批准数量减少到零,因为我们无法安全地增加批准数量,由于该功能存在一些安全风险;第二个批准是批准要存入所选代币的所需数量。然后,我们运行我们的DAX合约中的depositTokens()方法,将代币移动到托管地址,并在用户尚未拥有托管时创建一个托管。

提取函数简单地运行我们的DAX合约中的extractTokens()方法,将代币从托管转移到用户的地址,因为我们在那里不需要检查任何东西。

然后我们转向createLimitOrder()函数。还记得在DAX合约中它是多么复杂和庞大吗?嗯,在这种情况下,只是将正确的参数放在正确的位置。我们将在后面的render()函数中看到如何获取这些参数。与运行我们的DAX合约中的marketOrder方法相同,createMarketOrder()也是如此。

这是render()函数:

render() {
    return (
        <div className="main-container">
            <Sidebar
                firstSymbol={this.state.firstSymbol}
                secondSymbol={this.state.secondSymbol}
                balanceOne={this.state.balanceOne}
                balanceTwo={this.state.balanceTwo}
                deposit={(symbol, amount) => this.depositTokens(symbol, amount)}
                withdraw={(symbol, amount) => this.withdrawTokens(symbol, amount)}
                limitOrder={(type, firstSymbol, secondSymbol, quantity, pricePerToken) => this.createLimitOrder(type, firstSymbol, secondSymbol, quantity, pricePerToken)}
                marketOrder={(type, firstSymbol, secondSymbol, quantity) => this.createMarketOrder(type, firstSymbol, secondSymbol, quantity)}
            />
            <Orders
                buyOrders={this.state.buyOrders}
                sellOrders={this.state.sellOrders}
            />
            <History
                closedOrders={this.state.closedOrders}
            />
        </div>
    )
}

渲染函数生成三个组件:SidebarOrdersHistory。这些是我们之前创建的三个部分。在这种情况下,我们向每个组件添加了许多属性,以便轻松地传递数据。您可以看到,限价订单和市价订单属性只是接收参数并将它们发送到Main组件的函数中。

让我们来探索每个组件的实现方式;这是我自己的做法,所以您可以看到 DAX 应该是什么样子。我建议您根据您所学到的知识创建您自己的版本。以下是Sidebar组件;我们首先创建更新的constructor()bytes32()resetInputs()函数,这些函数将在渲染中使用:

class Sidebar extends React.Component {
 constructor() {
        super()
        this.state = {
            selectedLimitOrder: false,
            limitOrderPrice: 0,
            orderQuantity: 0,
        }
 }

    // To use bytes32 functions
 bytes32(name) {
        return myWeb3.utils.fromAscii(name)
 }

 resetInputs() {
        this.refs.limitOrderPrice.value = ''
        this.refs.orderQuantity.value = ''
        this.setState({
            limitOrderPrice: 0,
            orderQuantity: 0,
        })
 }

在这种情况下,render()函数有点过大,您可能无法理解它,因此我们将其分解为更小、更易消化的部分。因为我们想要给用户更多的选择,所以为每种代币添加了一个存款和提取按钮,以保持简单:

 render() {
        return (
            <div className="sidebar">
                <div className="selected-assets-title heading">Selected assets</div>
                <div className="selected-asset-one">{this.props.firstSymbol}</div>
                <div className="selected-asset-two">{this.props.secondSymbol}</div>
                <div className="your-portfolio heading">Your portfolio</div>
                <div className="grid-center">{this.props.firstSymbol}:</div><div className="grid-center">{this.props.balanceOne ? this.props.balanceOne : 'Loading...'}</div>
                <div className="grid-center">{this.props.secondSymbol}:</div><div className="grid-center">{this.props.balanceTwo ? this.props.balanceTwo : 'Loading...'}</div>
                <div className="money-management heading">Money management</div>
                <button className="button-outline" onClick={() => {
                    const amount = prompt(`How many ${this.props.firstSymbol} tokens do you want to deposit?`)
                    this.props.deposit(this.props.firstSymbol, amount)
                }}>Deposit {this.props.firstSymbol} </button>
                <button className="button-outline" onClick={() => {
                    const amount = prompt(`How many ${this.props.firstSymbol} tokens do you want to withdraw?`)
                    this.props.withdraw(this.props.firstSymbol, amount)
                }}>Withdraw {this.props.firstSymbol}</button>
                <button className="button-outline" onClick={() => {
                    const amount = prompt(`How many ${this.props.secondSymbol} tokens do you want to deposit?`)
                    this.props.deposit(this.props.secondSymbol, amount)
                }}>Deposit {this.props.secondSymbol} </button>
                <button className="button-outline" onClick={() => {
                    const amount = prompt(`How many ${this.props.secondSymbol} tokens do you want to withdraw?`)
                    this.props.withdraw(this.props.secondSymbol, amount)
                }}>Withdraw {this.props.secondSymbol}</button>

正如你所看到的,这些按钮通过prompt()全局 JavaScript 函数询问用户要移动多少代币,该函数提供了清晰但基本的动态输入。然后通过props将相应的函数调用传递到Main组件中。然后,我们可以添加buy按钮功能来格式化限价或市价订单所需的输入:

                <div className="actions heading">Actions</div>
                <button onClick={() => {
                    if(this.state.orderQuantity == 0) return alert('You must specify how many tokens you want to buy')
                    if(this.state.selectedLimitOrder) {
                        if(this.state.limitOrderPrice == 0) return alert('You must specify the token price at which you want to buy')
                        if(this.props.balanceTwo < (this.state.orderQuantity * this.state.limitOrderPrice)) {
                            return alert(`You must approve ${this.state.orderQuantity * this.state.limitOrderPrice} of ${this.props.secondSymbol} tokens to create this buy limit order, your ${this.props.secondSymbol} token balance must be larger than ${this.state.orderQuantity * this.state.limitOrderPrice}`)
                        }
                        // Buy the this.state.orderQuantity of this.props.firstSymbol
                        this.props.limitOrder(this.bytes32('buy'), this.bytes32(this.props.firstSymbol), this.bytes32(this.props.secondSymbol), this.state.orderQuantity, this.state.limitOrderPrice)
                    } else {
                        this.props.marketOrder(this.bytes32('buy'), this.bytes32(this.props.firstSymbol), this.bytes32(this.props.secondSymbol), this.state.orderQuantity)
                    }
                    this.resetInputs()
                }}>Buy {this.props.firstSymbol}</button>

卖出按钮做的事情是一样的,但是在顶层函数中使用卖出类型告诉智能合约我们想要卖出:

                <button onClick={() => {
                    if(this.state.orderQuantity == 0) return alert('You must specify how many tokens you want to sell')
                    if(this.state.selectedLimitOrder) {
                        if(this.state.limitOrderPrice == 0) return alert('You must specify the token price at which you want to sell')
                        if(this.props.balanceOne < this.state.orderQuantity) {
                            return alert(`You must approve ${this.state.orderQuantity} of ${this.props.firstSymbol} tokens to create this sell limit order, your ${this.props.firstSymbol} token balance must be larger than ${this.state.orderQuantity}`)
                        }
                        // Buy the this.state.orderQuantity of this.props.firstSymbol
                        this.props.limitOrder(this.bytes32('sell'), this.bytes32(this.props.firstSymbol), this.bytes32(this.props.secondSymbol), this.state.orderQuantity, this.state.limitOrderPrice)
                    } else {
                        this.props.marketOrder(this.bytes32('sell'), this.bytes32(this.props.firstSymbol), this.bytes32(this.props.secondSymbol), this.state.orderQuantity)
                    }
                    this.resetInputs()
                }} className="sell">Sell {this.props.firstSymbol}</button>

最后,我们给用户一个简单的选择输入,表示他们想要创建限价订单还是市价订单。如果他们选择了限价订单,将显示一个额外的输入来指示卖出或买入价格:

                <select defaultValue="market-order" onChange={selected => {
                    if(selected.target.value == 'limit-order') {
                        this.setState({selectedLimitOrder: true})
                    } else {
                        this.setState({selectedLimitOrder: false})
                    }
                }}>
                    <option value="market-order">Market Order</option>
                    <option value="limit-order">Limit Order</option>
                </select>
                <input ref="limitOrderPrice" onChange={event => {
                    this.setState({limitOrderPrice: event.target.value})
                }} className={this.state.selectedLimitOrder ? '' : 'hidden'} type="number" placeholder="Price to buy or sell at..." />
                <input ref="orderQuantity" onChange={event => {
                    this.setState({orderQuantity: event.target.value})
                }} type="number" placeholder={`Quantity of ${this.props.firstSymbol} to buy or sell...`} />
            </div>
        )
 }
}

与以前一样,我们有一个构造函数,一个bytes32函数和一个render()函数。resetInputs()函数负责清理输入字段,以便在购买或出售后重置它们的值。渲染最复杂的部分是创建我们的设计。主要逻辑可以在按钮中找到。我们在资金管理部分有四个按钮,用于存入 BAT 或 WAT 和取出 BAT 或 WAT。有一个简单的系统来管理你在托管账户中有多少代币。然后,有几个主要按钮用于买入或卖出。这些按钮中的每一个都运行createLimitOrdercreateMarketOrder方法,取决于您是否选择了限价订单下拉框还是其他选项。当您点击这些按钮时,组件读取存储在输入中的值,然后将它们传递给正确的函数。

仔细观察按钮背后的逻辑,了解它们如何决定调用哪个函数以及如何将信息传递到Main组件。现在让我们转向Orders组件,之前称为Trades

// The main section to see live trades taking place
class Orders extends React.Component {
 constructor() {
        super()
 }

 render() {
        let buyOrders = this.props.buyOrders
        let sellOrders = this.props.sellOrders
        if(buyOrders.length > 0) {
            buyOrders = buyOrders.map((trade, index) => (
                <div key={trade.id + index} className="trade-container buy-trade">
                    <div className="trade-symbol">{trade.firstSymbol}</div>
                    <div className="trade-symbol">{trade.secondSymbol}</div>
                    <div className="trade-pricing">{trade.type} {trade.quantity} {trade.secondSymbol} at {trade.price} {trade.secondSymbol} each</div>
                </div>
            ))
        }

        if(sellOrders.length > 0) {
            sellOrders = sellOrders.map((trade, index) => (
                <div key={trade.id + index} className="trade-container sell-trade">
                    <div className="trade-symbol">{trade.firstSymbol}</div>
                    <div className="trade-symbol">{trade.secondSymbol}</div>
                    <div className="trade-pricing">{trade.type} {trade.quantity} {trade.secondSymbol} at {trade.price} {trade.secondSymbol} each</div>
                </div>
            ))
        }
        return (
            <div className="trades">
                <div className="buy-trades-title heading">Buy</div>
                <div className="buy-trades-container">{buyOrders}</div>
                <div className="sell-trades-title heading">Sell</div>
                <div className="sell-trades-container">{sellOrders}</div>
            </div>
        )
    }
}

我们只有一个渲染和构造函数,从Main组件给出的买入或卖出订单对象生成我们所需的设计。除此之外,我们没有太多可说的,它为无尽的订单创建了一个清晰的界面。

现在,这是最后一个History组件:

// Past historical trades
class History extends React.Component {
 constructor() {
        super()
 }

 render() {
        let closedOrders = this.props.closedOrders
        if(closedOrders.length > 0) {
            closedOrders = closedOrders.map((trade, index) => (
                <div key={trade.id + index} className="historical-trade">
                    <div className={trade.type == 'sell' ? 'sell-trade' : 'buy-trade'}>{trade.type} {trade.quantity} {trade.firstSymbol} for {trade.quantity * trade.price} {trade.secondSymbol} at {trade.price} each</div>
                </div>
            ))
        }
        return (
            <div className="history">
                <div className="heading">Recent history</div>
                <div className="historical-trades-container">{closedOrders}</div>
            </div>
        )
    }
}

ReactDOM.render(<Main />, document.querySelector('#root'))

它几乎与Orders组件相同,但样式不同。记得运行ReactDOM.render()函数来显示你的 dApp。

大概就是这样了!现在你应该有一个可用的 DAX,可以使用并构建它,以创建一个更强大的交易所,因为你从内到外了解了它的工作方式。这可能是启动自己的交易所的最直接方式。这是一些交易后的样子:

总结

在本章中,你学会了如何从零开始构建一个 DAX,从交易所的工作原理的想法,到使用 react 和 truffle 构建用户界面,再到创建所需的智能合约,以便你亲眼看到一个完全去中心化的系统如何工作,最后将所有这些组合在一起,创建一个与你部署的合约和代币进行通信的漂亮 dApp。你了解了传统的、中心化的加密货币交易所与功能齐全的 DAX 之间的区别,以便你可以选择最适合你需求的类型。

在那个简短的介绍之后,你通过理解我们如何通过一系列智能合约实现交易和撮合引擎的技术方面,深入了解了 DAX 的技术方面。最后,你开发了一个简洁的界面,没有图表,以保持简单,并且通过可管理的组件集成了所有复杂的智能合约逻辑。

在下一章中,我们将探讨区块链上的机器学习,这可能是你听说过的话题,因为它结合了关于货币和计算未来的两种最流行的技术,通过构建一个允许我们使用智能合约中的线性回归算法训练模型来进行预测的 dApp。

第三部分:以太坊实现

本节的目标是利用区块链技术创建高级生产级项目,以便我们可以建立履历或进行众筹。

本节包括以下章节:

  • 第十章,以太坊区块链上的机器学习

  • 第十一章,创建基于区块链的社交媒体平台

  • 第十二章,创建基于区块链的电子商务市场

  • 第十三章,创建去中心化银行和借贷平台

第十章:以太坊区块链上的机器学习

区块链和人工智能是近年来最有趣的话题,这有充分的理由:它们是最先进的技术,已经被创造出来以颠覆大多数已建立的业务。我们能够教会计算机自己学习是一件非常强大的事情,这意味着未来的机器学习系统将继续发展。同样的道理适用于区块链:分布式计算领域刚刚开始,它将成为未来大多数问题的默认解决方案。那么为什么不将两者结合起来进行革命性的发明呢?事实证明它们很好地结合在一起,我们可以创建非常有趣的 dApps,从两个领域中受益,特别是通过利用它们来创建分布式市场,以解决机器学习问题,并奖励用户的计算能力。

在本章中,我们将涵盖以下主题:

  • 理解机器学习

  • 分布式机器学习市场

  • 构建智能合约机器学习市场

理解机器学习

机器学习ML)是人工智能AI)的一个子集,而后者又是数据科学更广泛主题中的一个领域。ML 专注于创建能够自己学习以解决特定问题的程序,而无需编写所有逻辑;我们只需要给它们大量的输入。试错是主要机制,机器慢慢地学会如何解决问题的正确输出。

计算机诞生的时刻,科学家们就问自己,“我们如何让这台机器像人类一样思考和行动?”这就是为什么了解计算机如何学习始于了解人类如何看待世界。

想一想:你认为动物和人类如何学会在我们生活的危险而混乱的世界中生存?通过向他人学习吗?嗯,那是一种有效的学习系统,但我们真正了解的一切都来自于在面对不确定性时进行实验。想象一下这样的场景:你处于一个原始世界,语言尚未发明——我们谈论的是数千年前。你在地上看到一个扁平而闪亮的红色物体,这对你来说是完全新的。你如何开始理解它呢?它可能是能够杀死你的东西,也可能是能够为你提供新材料的东西。你还不知道,所以你开始尝试不同的事情,始终保持警惕,因为你的主要目标是生存。你用一根棍子触摸它:没有反应。你用手触摸它:感觉温暖。你抓住它:感觉坚固,于是你试图打破它,没有成功。经过更多的实验,你得出结论:你手里拿着的是一个坚固的、自然形成的金属圆盘,你可以利用它的力量用太阳烹饪食物。

所有这些具体的知识都是通过试验使用试错机制得来的。我的观点是,这就是我们发现了当前世界上我们所知道的一切的方式,也是机器学习算法用来自行解决问题的系统。你给它们大量信息,它们就用它们的工具进行实验,这些工具通常是图像的逐像素读数和数据的字节,以生成结果。它们被用来预测未来,考虑到一些初始条件,以理解无法用经典编程解决的复杂问题,并创建帮助我们做得更好的工具。

从技术上讲,创建一个机器学习系统有三个步骤,如下所示:

  1. 收集关于一个主题的大量信息,例如 2,000,000 张独特的水瓶图片。

  2. 开发一个机器学习模型,生成所需的输出。在我们的例子中,假设我们想要创建一个模型,根据它们的形状、大小、颜色、化学成分和纯度对水瓶进行分类,因为我们需要找到最适合人类使用的水。这些属性被称为标签,因为它们是对每个组成部分的精确描述。

  3. 模型在一个称为训练的过程中消耗所有这些数据,在这个过程中,它调整了我们水瓶的每个组件的重要性,以计算哪些因素决定了最佳水的可能性。在某个时候,它将被训练,意味着它将理解构成最佳水的属性,生成一个我们可以使用的程序,以快速确定特定新水瓶的好坏。

这只是一个例子,说明了我们如何使用机器学习来提供复杂问题的解决方案,例如,我可以为了最佳健康状况消费什么样的水?一个危险的人看起来像什么?我怎样教我的相机判断它所看到的是狗还是猫?

一般来说,步骤是获取数据 -> 创建使用该数据创建程序的模型 -> 将程序用于特定情况。还有许多其他不同的系统,程序通过试错来自行获取数据。其他有趣的机器学习算法在生物水平上工作,教导机器人像真实生活中的动物一样行动,以便像它们一样学习和看待世界。

这是一个非常热门的话题,在未来几年将继续增长,以回答人类可能提出的最复杂的问题和疑问。这就是为什么我建议你探索广阔的人工智能世界。在将其与区块链相结合之前,看看有什么可以利用的东西。

分散式机器学习市场

我们将建立一个市场,用于购买和出售具有强大 GPU 的用户的计算能力,并希望帮助其他人执行机器学习,以教会他们的算法根据监督学习完成任务,其中程序学会根据给定目标从大量输入中生成所需输出,以便自我编程。

当我们需要处理我们的 ML 市场中发生的交易的永久记录以及买家根据其参数请求的训练模型时,以太坊就会介入其中,以便随时可以访问。其理念是创建一个地方,让全世界的人们可以开始通过新的硬件用途来赚钱,作为挖矿的替代方案,同时还提供了一个安全的 ML 算法系统。

我们将使用 GPU 来训练我们的机器学习程序,因为它们非常擅长同时处理大量并行操作,这样我们就可以快速处理大批输入,比使用 CPU 更快。我们还将使用以太坊作为默认支付货币,以便轻松处理分散式交易。

如今,大多数机器学习模型都是基于神经网络NN)的,这是对人脑工作原理的抽象,转化为计算机语言。它基于虚拟的个体神经元,接收输入并在满足条件时产生输出。例如,假设一个简单的神经元包含以下语句:

if(input > 10) return output = true;

如果输入大于 10,则该语句将返回一个正值。这个函数被称为激活函数,因为只有在函数满足条件时才会激活。我们可以使用不同的参数和配置将许多这些神经元组合在一起,得到所谓的神经网络,它可以处理复杂的输入以生成精确的输出。在训练时,我们会重新调整激活函数以更好地适应我们的目标。一旦设置好了我们的模型,这一切都会自动完成。最终,我们得到了一个经过训练的程序,能够回答复杂的问题,而无需编写每个特定情景的代码。

一旦模型从我们的训练数据集中调整好,我们就可以用来自不同来源的新输入来测试它,以确定它是否生成了最优输出。这很重要,因为存在过拟合的风险,即机器学习程序进行了过多的优化,变得过于特定于我们的初始输入,这样就无法从新数据中产生有效结果。这就像一个必须从头开始成为全科医生的外科医生:它不会产生很好的结果,因为它太专业化了。

一些著名的激活函数是 Sigmoid 和 ReLU。深度学习是将多层神经元堆叠在一起的过程,以便神经元的输出传递到另一个神经元,从而获得更高级的结果。这些网络被称为深度神经网络DNNs),因为它们由多层组成。一定要自己探索神奇的神经网络世界,了解未来技术是如何塑造的。

在这里我们不会使用神经网络,因为由于区块链的限制,从头开始在Solidity上实现它们很困难,所以我们将使用您可以根据需要扩展的更简单的算法。这是我们的协议将如何工作的简要说明:

  1. 用户向智能合约以以太币的形式发布一组数据、一个评估函数(我们的机器学习模型)以及完成任务的奖励。

  2. 那些希望完成任务的人将从第一个用户那里下载发布的数据,以训练给定的机器学习模型,以生成一个训练良好的程序,然后将其返回给智能合约。

  3. 外部用户将查看针对该特定任务发布的所有解决方案,以确定谁是赢家。买家将根据自己的偏好确定赢家。

从这个协议中,我们可以建立用户将遵循的以下流程:

  1. 买家,即想要训练他们的模型的人,部署一个智能合约,其中包含以下数据:

    • 构造函数中的模型定义——例如,DNN。

    • 要训练的数据集——例如,由 30 x 30 像素制成的手写数字图像数组。每个图像都是一个 30 x 30 像素(900 像素)的数组,其中每个像素又是另一个数组,包含有关像素位置以及它是黑色还是白色的信息(我们不希望在这个图像中使用颜色,以避免复杂性)——例如 [[0, true], [1, false]] 将表示一个 2 x 1 像素的图像,其中第一个像素是黑色,而另一个是白色。这些数据集将发布到一个外部网站,人们可以自由访问以训练模型。在我们的构造函数中,我们将提供一个 URL,即https://example.com/dataset

    • 训练模型的奖励以以太坊支付,并在可支付的构造函数中设置了此安排。

  2. 合约被发布,卖家开始参与训练模型的任务。从数据集中,90%的数据将用于训练模型,而剩下的 10%将用于测试程序的结果,以验证其准确性。为了确保卖家不会彼此抄袭,将向不同的参与者提供不同的随机数据集。

  3. 买家决定哪个模型对他们最有效,并选择一个赢家。如果到达到期时间而买家尚未选择赢家,则第一个参与者将获得奖励。

对于我们的机器学习市场,我们将在 Solidity 中使用一个简单的线性回归机器学习算法。用户将提交包含名称和两个数字参数以进行预测的数据。线性回归是两个因素之间的关系,例如,网站销售量与访问者数量。在这种情况下,我们可以建立一个模型,使我们能够预测给定访问者数量的销售量。

简单线性回归模型可以应用于许多领域,其中一个变量取决于另一个变量,它是可用的最简单的机器学习系统之一。这就是为什么我们将使用它的原因,因为重要的是能够在 Solidity 中重新创建它,以验证其他用户提供的解决方案。理想情况下,我们将实现一个 NN 或更复杂的模型,但考虑到区块链的限制,这将需要太多的时间来开发。您可以借鉴本章的教训来扩展市场。在下一节中,您将学习如何创建市场所需的代码。

构建智能合约机器学习市场

我们的机器学习市场将专门使用线性回归算法,以简化流程,让您了解它们如何紧密联系。我鼓励您扩展解决方案,以练习您的机器学习和区块链技能。要应用简单的线性回归算法,我们需要以下内容:

  • 一个预测函数,用于从数据中生成预测

  • 一个组合预测结果的成本函数

  • 用于训练我们的算法的优化算法使用梯度下降,这将微调预测以获得更精确的结果

  • 一个训练函数来改善我们的算法

预测函数

首先,您需要了解,我们的简单线性回归算法使用以下函数预测值:

y = weight * x + bias

如果我们根据网站访问者数量预测销售量,我们的预测函数将如下所示:

Sales = weight * visitors + bias

我们的目标是获得固定的权重和偏差值,优化我们的预测函数,以便我们获得销售的真实估计。例如,经过训练的线性回归会是这样的:

Sales = 0.43 * visitors + 0.9

我们在给定数据集上训练后得到了0.43的权重和0.9的偏差。我们应该能够使用该优化函数来为我们特定的需求做出准确的预测,从而取得出色的结果。我们需要在 Python 和 Solidity 中实现预测函数,因为卖家将使用 Python 训练模型,而我们将使用 Solidity 来验证这些卖家给出的结果。以下是我们市场的 Python 和 Solidity 中的prediction函数的样子:

# Python implementation
def prediction(x, weight, bias):
    return weight * x + bias

供您参考,这是我们将添加的 Solidity 函数,允许卖家和买家通过进行预测来验证模型的准确性:

// Solidity implementation
function prediction(uint256 _x, uint256 _weight, uint256 _bias) public pure returns(uint256) {
 return _weight * _x + _bias;
}

成本函数

要训练我们的线性回归算法以生成准确的预测,我们需要一个成本函数。成本函数是分析我们的预测函数在数据集中工作效果如何的一种方法。它给了我们一个错误率,这实际上是真实结果与预测之间的差异。错误越小,我们做出的预测就越好。成本函数将真实结果和预测作为输入,输出我们模型的错误,如下所示:

error = result - prediction

在我们的案例中,有许多不同类型的成本函数。在这种情况下,我们将使用均方误差MSE)成本函数,它看起来像这样:

error = sum((result - prediction)2) / numberOfDataPoints

为了使其更清晰,我们可以添加具有所有参数的预测函数,以便您可以看到变量在成本函数中的作用,如下面的代码所示:

error = sum((result - prediction(x, weight, bias))2) / numberOfDataPoints

在这里,sum()是所有真实结果减去预测的平方的总和,所有结果数据集的总和。所有这些都被数据点的数量除以。请记住,result是我们试图预测的实际值。例如,回到我们之前的例子,我们试图预测每位访客将获得多少销售额,result将是10个销售额,这来自 200 位访客,而预测是我们从权重和偏差得出的估计值。

为了帮助您更好地理解该函数,考虑下面的假想数据集的示例:一个国家的假枪支持有和每个国家的犯罪数;在这个例子中,我们有兴趣了解枪支数量如何影响每个国家的犯罪数。利用这些数据,我们可以预测犯罪,以便我们可以调动特定数量的警察来处理这些情况。请记住,这是虚假数据,用来说明成本函数的工作原理:

国家 枪支总数 每年犯罪数量
德国 3,520 20
爱沙尼亚 192 3
巴哈马 91 0
巴西 9,271 88

我们首先用随机权重和偏差初始化我们的预测函数,如下所示的代码:

// Our prediction function definition for you to remember how it looked like
y = weight * x + bias

// Our prediction function with random weight and bias
prediction = 0.1 * x + 0.4

德国的犯罪预测如下所示:

prediction = 0.1 * 3520 + 0.4 = 352.4 crimes per year

我们得到了352.4起犯罪,我们可以近似为 352,因为用小数点谈论犯罪没有意义。正如你所看到的,我们使用该权重和偏差的预测比每年 20 起犯罪的真实结果要高,因为我们的模型尚未训练,所以预计会有巨大的差异。

然后我们计算所有这些值的成本函数。让我们看看德国的情况如何:

// Our cost function definition for you to remember how it looked like
error = sum((result - prediction)2) / numberOfDataPoints

// Our cost function for the initial dataset
error = sum((20 - 352)2) / 1

我们正在将成本函数应用于一个数据点,以查看初始预测的错误,以便您可以看到它是如何应用的。这是结果:

error = (20 - 352)2 / 1 = 110224 

误差为110224,这是一个巨大的数字,因为我们将其应用于一个数据点,而且我们的模型尚未训练。现在对所有数据点执行相同操作,直到您为整个数据集生成误差。希望您可以通过该示例了解计算该误差的过程。

我们需要计算误差,以优化我们的预测函数,以便稍后进行更准确的预测。现在概念已经清楚,我们可以在 Python 中实现该函数。在 Solidity 中,我们希望它能够从特定市场解决方案计算误差,以便丢弃那些具有过大误差的市场解决方案。Python 中的cost函数将被买家用于验证其训练结果,并且将被 Solidity 中的卖家用于验证提交。让我们看看以下代码:

# The cost function implemented in python
def cost(results, weight, bias, xs):
    error = 0.0
    numberOfDataPoints = len(xs)
    for i in range(numberOfDataPoints):
        error += (results[i] - (weight * xs[i] + bias)) ** 2
    return error / numberOfDataPoints

xs 参数是独立变量的数组,x——我们在预测函数中看到的。在 Solidity 中它看起来是这样的;因为它是一个纯函数,我们不用担心燃气成本,因为一切都将在本地执行,而不必从区块链修改状态:

// The cost function implemented in solidity
function cost(int256[] memory _results, int256 _weight, int256 _bias, int256[] memory _xs) public pure returns(int256) {
    require(_results.length == _xs.length, 'There must be the same number of _results than _xs values');
    int256 error = 0; // Notice the int instead of uint since we want negative values too
    uint256 numberOfDataPoints = _xs.length;
    for(uint256 i = 0; i < numberOfDataPoints; i++) {
        error += (_results[i] - (_weight * _xs[i] + _bias)) * (_results[i] - (_weight * _xs[i] + _bias));
    }
    return error / int256(numberOfDataPoints);
}

正如您所看到的,我们将预测函数包含在for循环中,以计算结果减去预测的平方,以便我们可以从cost函数计算误差。这将由希望优化买家的特定线性回归的卖家使用,以进行准确的预测。

优化算法

现在我们可以在给定一些参数的情况下进行预测,并使用成本函数计算这些预测的精度,我们必须努力改进这些预测,通过减小由成本函数生成的误差。我们如何减小成本函数生成的误差?通过使用优化算法调整我们的预测函数的权重和偏差。在这种情况下,我们将使用梯度下降,这使我们能够不断减小误差。以下是说明其工作原理的图表:

我们从由随机权重和偏差值引起的高误差开始,然后通过优化这些参数来减小误差,直到我们达到足够好的预测模型,即图表中的局部最小值。想法是计算权重偏差的偏导数,看它们如何影响最终预测,直到我们达到最小值。我们不会探讨计算这些偏导数的数学,因为它可能导致混淆,因此带有偏导数的结果函数如下所示:

weightDerivative = sum(-2x * (result - (x * weight + bias))) / numberOfDataPoints

biasDerivative = sum(-2 * (result - (x * weight + bias))) / numberOfDataPoints

让我们来看看更新机器学习算法的权重和偏差的这些函数的实现:

# Python implementation, returns the optimized weight and bias for that step
def optimizeWeightBias(results, weight, bias, xs, learningRate):
    weightDerivative = 0
    biasDerivative = 0
    numberOfDataPoints = len(results)
    for i in range(numberOfDataPoints):
        weightDerivative += (-2 * xs[i] * (results[i] - (xs[i] * weight + bias)) / numberOfDataPoints)
        biasDerivative += (-2 * (results[i] - (xs[i] * weight + bias)) / numberOfDataPoints)

    weight -= weightDerivative * learningRate
    bias -= biasDerivative * learningRate
    return weight, bias

在 Solidity 中,它看起来像这样:

// Solidity implementation
function optimize(int256[] memory _results, int256 _weight, int256 _bias, int256[] memory _xs, int256 _learningRate) public pure returns(int256, int256) {
    require(_results.length == _xs.length, 'There must be the same number of _results than _xs values');
    int256 weightDerivative = 0;
    int256 biasDerivative = 0;
    uint256 numberOfDataPoints = _xs.length;
    for(uint256 i = 0; i < numberOfDataPoints; i++) {
        weightDerivative += (-2 * _xs[i] * (_results[i] - (_xs[i] * _weight + _bias)) / int256(numberOfDataPoints));
        biasDerivative += (-2 * (_results[i] - (_xs[i] * _weight + _bias)) / int256(numberOfDataPoints));
    }
    _weight = weightDerivative * _learningRate;
    _bias = biasDerivative * _learningRate;
    return (_weight, _bias);
}

如你所见,我们通过使用前面代码块中描述的函数来计算两个导数,以便我们可以使用优化后的值更新权重和偏置。学习速率是我们达到图表最小点的步长大小。如果我们迈出的步子太大,我们可能会错过最小值,如果我们迈出的步子太小,可能需要太长时间才能到达那个最小值。无论如何,最好保持一个平衡的学习速率并尝试不同的步长。现在我们有了改进我们预测函数的方法。

训练函数

我们可以开始通过一个新的函数来改进我们的模型,该函数循环执行多个优化调用,直到达到最小值,此时模型将完全优化。代码如下所示:

# Python implementation
def train(results, weight, bias, xs, learningRate, iterations):
 error = 0 for i in range(iterations):
    weight, bias = optimizeWeightBias(results, weight, bias, xs, learningRate)
    error = cost(results, weight, bias, xs)
    print("Iteration: {}, weight: {:.4f}, bias: {:.4f}, error: {:.2}".format(i, weight, bias, error))
 return weight, bias

Solidity 实现看起来非常相似,尽管我们必须确保结果和独立变量的值具有相同的长度,以避免错误,如下面的代码所示:

// Solidity implementation
function train(int256[] memory _results, int256 _weight, int256 _bias, int256[] memory _xs, int256 _learningRate, uint256 _iterations) public pure returns(int256, int256) {
    require(_results.length == _xs.length, 'There must be the same number of _results than _xs values');
    int256 error = 0;
    for(uint256 i = 0; i < _iterations; i++) {
        (_weight, _bias) = optimize(_results, _weight, _bias, _xs, _learningRate);
        error = cost(_results, _weight, _bias, _xs);
    }
    return (_weight, _bias);
}

如你所见,我们正在使用优化函数和成本函数连续减少误差,通过更新权重和偏置参数来进行指定次数的迭代。

现在你应该能够创建和训练线性回归模型,使用train函数训练你的模型后,使用预测函数进行预测。以下是完整的 Python 代码供你参考,尽管你可以在官方 GitHub 上查看更新版本,链接为github.com/merlox/machine-learning-ethereum/blob/master/linearRegression.py

我们首先创建构造函数,该构造函数将使用uniform库训练模型,并使用初始随机值,因为它返回 0 到 1 之间的浮点数,如下面的代码所示:

from random import uniform

class LinearRegression:
    xs = [3520, 192, 91, 9271]
    results = [20, 3, 0, 88]

    def __init__(self):
        initialWeight = uniform(0, 1)
        initialBias = uniform(0, 1)
        learningRate = 0.00000004
        iterations = 2000
        print('Initial weight {}, Initial bias {}, Learning rate {}, Iterations {}'.format(initialWeight, initialBias, learningRate, iterations))
        finalWeight, finalBias = self.train(self.results, initialWeight, initialBias, self.xs, learningRate, iterations)
        finalError = self.cost(self.results, finalWeight, finalBias, self.xs)
        print('Final weight {:.4f}, Final bias {:.4f}, Final error {:.4f}, Prediction {:.4f} out of {}, Prediction Two {:.4f} out of {}'.format(finalWeight, finalBias, finalError, self.prediction(self.xs[1], finalWeight, finalBias), self.results[1], self.prediction(self.xs[3], finalWeight, finalBias), self.results[3]))

然后,我们实现predictioncost函数,就像你刚学的一样,放在构造函数下面,如下面的代码所示:

    # Python implementation
    def prediction(self, x, weight, bias):
        return weight * x + bias

    # The cost function implemented in python
    def cost(self, results, weight, bias, xs):
        error = 0.0
        numberOfDataPoints = len(xs)
        for i in range(numberOfDataPoints):
            error += (results[i] - (weight * xs[i] + bias)) ** 2
        return error / numberOfDataPoints

然后,我们添加了优化的权重和偏置函数,如下面的代码所示:

    # Python implementation, returns the optimized weight and bias for that step
    def optimizeWeightBias(self, results, weight, bias, xs, learningRate):
        weightDerivative = 0
        biasDerivative = 0
        numberOfDataPoints = len(results)
        for i in range(numberOfDataPoints):
            weightDerivative += -2 * xs[i] * (results[i] - (xs[i] * weight + bias))
            biasDerivative += -2 * (results[i] - (xs[i] * weight + bias))

        weight -= (weightDerivative / numberOfDataPoints) * learningRate
        bias -= (biasDerivative / numberOfDataPoints) * learningRate

        return weight, bias

最后,我们通过在类的作用域之外创建train函数并初始化类来完成代码,如下面的代码所示:

    # Python implementation
    def train(self, results, weight, bias, xs, learningRate, iterations):
        error = 0
        for i in range(iterations):
            weight, bias = self.optimizeWeightBias(results, weight, bias, xs, learningRate)
            error = self.cost(results, weight, bias, xs)
            print("Iteration: {}, weight: {:.4f}, bias: {:.4f}, error: {:.2f}".format(i, weight, bias, error))
        return weight, bias

# Initialize the class
LinearRegression()

如你所见,我们创建了一个 Python 类,在构造函数中运行train函数。如果你对 Python 不熟悉,不要担心;你只需要理解这段代码正在训练我们的线性回归算法进行更精确的计算。创建一个名为linearRegression.py的文件,并将代码写入其中。然后你可以用以下命令行运行它:

python linearRegression.py

你会看到程序不断通过向最小值迈出小步骤来减少误差,直到它达到一个不太改善的程度。这没关系:我们希望它能做出精确的预测,但不一定 100% 准确。然后,你可以用最终的权重和偏置来对那个机器学习模型进行预测。

让我们看一下智能合约市场,看看用户将如何与之交互。我们的目标是提供一个地方,让机器学习开发人员可以上传其模型,并以以太币支付,目的是从几个卖家中获得解决方案,然后根据错误或买家的选择选择一个赢家。让我们看一下以下代码:

pragma solidity 0.5.5;

contract MachineLearningMarketplace {}

我们可以开始添加变量来创建我们想要的应用程序,如下所示:

pragma solidity 0.5.5;

contract MachineLearningMarketplace {
    event AddedJob(uint256 indexed id, uint256 indexed timestamp);
    event AddedResult(uint256 indexed id, uint256 indexed timestamp, address indexed sender);
    event SelectedWinner(uint256 indexed id, uint256 indexed timestamp, address indexed winner, uint256 trainedIdSelected);

    struct Model {
        uint256 id;
        string datasetUrl;
        uint256 weight;
        uint256 bias;
        uint256 payment;
        uint256 timestamp;
        address payable owner;
        bool isOpen;
    }
    mapping(uint256 => Model) public models;
    mapping(uint256 => Model[]) public trainedModels;
    uint256 public latestId;
}

我们添加了三个事件来通知用户已添加了新作业或结果,以及何时选择了提案的获胜者。这样,人们就会在他们的提案被更新时收到通知。然后,我们有一个名为 Model 的结构体,它代表我们希望的线性回归 ML 模型,其中包括数据集、权重、偏差和支付等重要变量。最后,我们添加了一对映射,以对买家创建的模型(那些支付来让他们的模型训练)和卖家创建的模型进行排序,后者训练数据集并上传特定的权重和偏差,以便在被买家选中时赢取。latestId 是一个标识符,表示哪个模型是最新的。

开放的模型意味着它仍在运行,因此您可以发送提案并参与其中,以获得被选中的机会。如果它已关闭,您仍然可以参与,但要知道您将无法获胜,因为获胜者已经被选定。

让我们继续讨论我们 ML 市场的三个最重要的功能。上传作业功能如下所示:

/// @notice To upload a model in order to train it
/// @param _dataSetUrl The url with the json containing the array of data
function uploadJob(string memory _dataSetUrl) public payable {
    require(msg.value > 0, 'You must send some ether to get your model trained');
    Model memory m = Model(latestId, _dataSetUrl, 0, 0, msg.value, now, msg.sender, true);
    models[latestId] = m;
    emit AddedJob(latestId, now);
    latestId += 1;
}

这是上传结果功能,其中添加了一些文档以澄清内部使用的参数:

/// @notice To upload the result of a trained model
/// @param _id The id of the trained model
/// @param _weight The final trained weight, it must be with 10 decimals meaning that 1 weight is 1e10 so that you can do smaller fractions such as 0.01 which would be 1e8 or 100000000
/// @param _bias The final trained bias, it must be with 10 decimals as the weight
function uploadResult(uint256 _id, uint256 _weight, uint256 _bias) public {
    Model memory m = Model(_id, models[_id].datasetUrl, _weight, _bias, models[_id].payment, now, msg.sender, true);
    trainedModels[_id].push(m);
    emit AddedResult(_id, now, msg.sender);
}

最后,这是选择结果功能,因为我们必须确保作业是开放的,并且尚未选择赢家,所以这个函数相当冗长。如果三天后没有选择获胜者,第一个申请人将赢得奖励,以避免失去以太币:

/// @notice To choose a winner by the sender
/// @param _id The id of the model
/// @param _arrayIdSelected The array index of the selected winner
function chooseResult(uint256 _id, uint256 _arrayIdSelected) public {
    Model memory m = models[_id];
    Model[] memory t = trainedModels[_id];
    require(m.isOpen, 'The job must be open to choose a result');
    // If 3 days have passed the winner will be the first one, otherwise the owner is allowed to choose a winner before 3 full days
    if(now - m.timestamp < 3 days) {
        require(msg.sender == m.owner, 'Only the owner can select the winner');
        t[_arrayIdSelected].owner.transfer(m.payment);
        models[_id].isOpen = false;
        emit SelectedWinner(_id, now, t[_arrayIdSelected].owner, t[_arrayIdSelected].id);
    } else {
        // If there's more than one result, send it to the first
        if(t.length > 0) {
            t[0].owner.transfer(m.payment);
            emit SelectedWinner(_id, now, t[0].owner, t[0].id);
        } else {
            // Send it to the owner if none applied to the job
            m.owner.transfer(m.payment);
            emit SelectedWinner(_id, now, msg.sender, 0);
        }
        models[_id].isOpen = false;
    }
}

uploadJob 函数将由买家使用,以发布他们的数据集和付款,以便让全世界的参与者训练他们的模型。uploadResult 函数将由卖家使用,以获取有关训练指定数据集直到错误最小化的作业的信息。最后,chooseResult 函数是由买家用于选择确定作业的赢家提案的函数。作业的创建者有三天的时间选择获胜提案。如果三天后没有人申请,那么支付将退还给所有者。如果有参与者,但所有者尚未选择获胜者,则奖励将作为对速度的补偿发送给第一个参与者;在这种情况下,此函数必须由外部用户执行以执行支付。

这些是构成我们 ML 市场的主要组件;然而,我们需要一些函数来帮助人们与之交互。以下是添加到 ML 市场的新函数,为了更好地帮助您理解,将它们分解成片段。

首先,我们创建了完整文档的成本函数,这样我们就能理解它的作用:

/// @notice The cost function implemented in solidity
/// @param _results The resulting uint256 for a particular data element
/// @param _weight The weight of the trained model
/// @param _bias The bias of the trained model
/// @param _xs The independent variable for our trained model to test the prediction
/// @return int256 Returns the total error of the model
function cost(int256[] memory _results, int256 _weight, int256 _bias, int256[] memory _xs) public pure returns(int256) {
    require(_results.length == _xs.length, 'There must be the same number of _results than _xs values');
    int256 error = 0; // Notice the int instead of uint since we want negative values too
    uint256 numberOfDataPoints = _xs.length;
    for(uint256 i = 0; i < numberOfDataPoints; i++) {
        error += (_results[i] - (_weight * _xs[i] + _bias)) * (_results[i] - (_weight * _xs[i] + _bias));
    }
    return error / int256(numberOfDataPoints);
}

然后我们有获取模型函数来检索结构模型中包含的变量,因为我们目前无法原样返回结构。我们必须做这些类型的技巧来独立获取结构值。以下代码显示了该函数:

/// @notice To get a model dataset, payment and timestamp
/// @param id The id of the model to get the dataset, payment and timestamp
/// @return Returns the dataset string url, payment and timestamp
function getModel(uint256 id) public view returns(string memory, uint256, uint256) {
    return (models[id].datasetUrl, models[id].payment, models[id].timestamp);
}

然后我们添加另一个获取器函数,它为特定 ID 的所有经过训练的模型提供了,如下所示的代码。对于想要查看他们特定作业收到了什么提案的卖家来说,这是很有用的。如果我们要在一个 dApp 中实现这个机器学习市场,我们将不得不为作业和其他映射添加一些更多的获取器:

/// @notice To get all the proposed trained models for a particular id
/// @param _id The id of the model created by the buyer
/// @return uint256[], uint256[], uint256[], uint256[], address[] Returns all those trained models separated in arrays containing ids, weights, biases, timestamps and owners
function getAllTrainedModels(uint256 _id) public view returns(uint256[] memory, uint256[] memory, uint256[] memory, uint256[] memory, address[] memory) {
    uint256[] memory ids;
    uint256[] memory weights;
    uint256[] memory biases;
    uint256[] memory timestamps;
    address[] memory owners;
    for(uint256 i = 0; i < trainedModels[_id].length; i++) {
        Model memory m = trainedModels[_id][i];
        ids[i] = m.id;
        weights[i] = m.weight;
        biases[i] = m.bias;
        timestamps[i] = m.timestamp;
        owners[i] = m.owner;
    }
    return (ids, weights, biases, timestamps, owners);
}

我们有一个cost函数,用于快速验证由拟议销售方上传的结果,一个getModel函数,主要由想要获取有关模型更多具体信息的卖家使用,以及一个getAllTrainedModels函数,返回特定工作的参与者。请注意,我们返回结构中最重要的变量而不是整个结构。我们这样做的简单原因是,目前在 Solidity 中我们无法返回结构,所以我们必须分开每个变量,并为每个变量返回一个数组。

这个市场的一般工作流程如下:

  1. 拥有要训练的机器学习模型的买家使用uploadJob函数将其数据集和付款上传到市场。

  2. 生成了一个AddedJob事件,通知对该市场新工作感兴趣的用户。他们可以使用web3或外部 dApps 来监听这些事件,因为合约是开源的。

  3. 卖家使用getModel函数读取模型数据,特别是时间戳,因为那是最重要的信息片段,使用他们从事件中收到的id模型。然后他们开始使用我们之前构建的 Python 应用程序或他们自己的应用程序进行模型训练,因为有许多不同的方法可以训练线性回归算法。

  4. 他们使用uploadResult函数将他们训练好的权重和偏置上传到该作业作为一个新的提案。这将触发AddedResult事件,通知买方是否在听取更新,以便他们可以选择获胜者。

  5. 在任务创建后不到三天之内,买家会浏览提案,比较每个提案产生的错误与cost函数或他们自己的实现。他们几乎肯定会选择错误最小的结果,尽管他们可以选择任何一个。选择完毕后,模型的状态将变为isOpen = false,这意味着赢家已选定,并且会触发SelectedWinner事件。

就是这样!您现在能够在区块链上上传和训练线性回归模型了。

总结

在这一章中,您学到了结合区块链和机器学习的基本实用性,因为它们几乎是对立的,这意味着它们互补,可以很好地实现最佳的安全性和性能。我们从机器学习的一般解释开始,这样您就可以通过快速了解生成和训练机器学习模型的过程来理解所有的炒作。然后我们深入探讨了应用的技术功能,这样您就能清楚地看到机器学习和区块链的交汇点。最后,我们建立了机器学习市场,因为这是两种技术的绝佳结合。您看到了线性回归算法如何在 Python 和 Solidity 中逐步实现。我们建立了市场,全世界的用户可以在这里为每个任务训练和交换计算资源,创建了一个伟大的安全开放源代码平台,人们在这里可以自由互动,没有审查、费用或中心化。

在下一章中,我们将探索类似于本章中所见的高级以太坊实现,但涉及到不同的行业,从一个基于区块链的社交媒体平台开始,它将分散化和互联网上的社交互动结合在一起。

第十一章:创建基于区块链的社交媒体平台

掌握以太坊开发始于大量的理论和技术,但在某一时刻,你必须跨出一步,开始将你最近获得的知识应用于构建你的投资组合的实际情况。这就是为什么我们要创建一个基于区块链的社交媒体平台,因为它是区块链技术的最佳用例之一,因为我们提供了人们的信任。不幸的是,许多中心化的社交媒体公司正在滥用这种信任,通过窃取和变现用户的隐私。诸如 Twitter 或 Facebook 等社交媒体平台之所以著名,是因为它们赋予了人们在一个界面上与许多个体保持联系的权力,利用了互联网的能力。

本章将带您了解如何创建一个完全基于区块链而无需中心化服务器的动态社交媒体平台的挑战。您将了解如何使用 React 创建一个漂亮的用户界面。然后,您将探索如何更好地组织信息,以便使用智能合约允许人们找到他们想要的内容。最后,您将使用 web3 将所有内容联系在一起,并能够使用您的社交媒体平台。

在本章中,我们将涵盖以下主题:

  • 理解分散的社交媒体

  • 创建用户界面

  • 构建智能合约

  • 完成 dApp

理解分散的社交媒体

当涉及基于以太坊的社交媒体 dApp 时,我们帮助人们解决了许多目前中心化公司尚无法有效解决的问题。我们可以帮助解决以下问题:

  • 在分散的区块链上保护用户的隐私

  • 通过不允许来自外部中心化实体的审查来保证完全的自由,因为区块链上的信息是永久的

  • 一个不变的、固定的存储系统,在几十年后仍然可以访问创建的内容

然而,当我们考虑构建一个分散的社交媒体平台时,我们会失去一些对现代应用程序至关重要的以下重要方面:

  • 速度:用户无法像使用普通的中心化应用程序一样快速使用 dApp,因为它依赖于一个庞大而缓慢的互联网络。

  • 存储限制:以太坊的空间有限,因此每个字节都很昂贵,导致在区块链上可以存储的内容受到巨大限制,因此我们必须找到克服这些自然限制的方法,同时尽可能保持分散。

  • Gas 费用:普通的中心化应用程序不必为系统上每个操作支付 gas 费用,因为它们了解到所有这些成本都是在中心化服务器上支付的。在区块链上,每个交易都有一定的成本,这可能是显着的。我们将通过使用测试网络来解决这个问题,在测试网络上,gas 没有价值,直到最终的应用程序创建完成。

另一个大问题是我们无法在区块链上存储图像和视频;如果我们希望保留主系统的去中心化,我们将不得不依赖于分布式存储解决方案,例如 IPFS,但这并不是强制性的。

初始概念

我们的目标是创建一个有效的社交媒体平台,克服或完全避免区块链的限制。为了简化我们的 dApp 的复杂性,我们将构建一个类似于 Twitter 的应用程序,用户只分享文本消息,而不提供分享图像或视频的选项。

由于我们是开发人员,我们将创建一个面向程序员、设计师、开发人员和各种技术相关领域的 Twitter,让人们可以在共同兴趣的社区中感到受欢迎。我们希望它具有以下功能:

  • 仅受智能合约容量限制的文本字符串分享能力

  • 为每个内容添加标签的能力

  • 通过点击标签来查看人们在他们的内容中包含的标签的功能

  • 订阅标签的功能

我们不希望人们关注其他人,我们只会给他们提供关注标签的能力,这样他们就会专注于内容而不是信使。让我们开始着手设计用户界面,这将成为我们的社交媒体 dApp,供技术爱好者通过标签而不是特定用户来关注内容。

创建用户界面

此特定项目的用户界面将围绕内容和标签展开,因为标签是用户发现新趋势内容的方式。用户将能够订阅特定标签,以在他们的订阅中接收来自这些主题的内容。

像往常一样,我们首先用 Truffle 设置一个新项目。按照以下步骤设置你的项目:

  1. 克隆初创仓库(github.com/merlox/dapp),其中包含了在你的 React dApp 上工作的初始配置:
git clone https://github.com/merlox/dapp
  1. 将仓库重命名为social-media-dapp以整理内容:
mv dapp/ social-media-dapp/
  1. 通过访问 GitHub 创建一个新的空仓库(不包含许可证或.gitignore,因为它们已经包含在你的项目中),并使用以下命令来更新拉取/推送 URL:
git config remote.origin.url https://<YOUR-USERNAME>:<YOUR-PASSWORD>@github.com/<YOUR-USERNAME>/social-media-dapp
  1. 推送第一个提交。使用npm i安装依赖项,并使用webpack -wd运行webpack

  2. 通过运行静态服务器http-server dist/来打开你的应用程序,并访问http://localhost:8080,查看是否一切都设置正确。

现在你可以开始创建你的用户界面了。你已经知道如何做了,那为什么不先自己试试呢?你会惊讶于此时你能够做到的事情,所以我鼓励你尝试建立自己的系统。我们的想法是通过指导你的步骤来一起构建这个 dApp,直到你拥有一个高质量的 dApp,可以用来建立你的简历或进一步为 ICO 或作为人类进步的开源软件的开发。

配置 webpack 样式

最后,你将必须有两个部分:一个是最受欢迎的标签,这将来自我们智能合约中的映射,另一个是你可以在其中阅读更多关于每个具体标签的内容,同时能够发布内容。你可能想设置样式加载程序以能够在你的 dApp 上使用 CSS,这在你刚刚克隆的默认 dApp 上并没有设置。为了这样做,在停止 webpack 后安装以下依赖项:

npm i -S style-loader css-loader

现在你已经安装了所需的库,可以在项目中使用 CSS 文件,你可以通过在css文件的loaders块中添加一个新的 loader 来更新 webpack 配置文件。请注意,我们使用了两个 loaders,style-loader排在第一个。否则它将无法工作:

{
    test: /\.css$/,
    exclude: /node_modules/,
    use: [
        { loader: 'style-loader' },
        { loader: 'css-loader' }
    ]
}

建立初始结构

打开index.js文件,并开始创建你的用户界面。首先,通过创建一些以后会用到的必要变量来设置构造函数:

  1. 为任何 React 项目设置所需的导入,以及我们现在可以通过样式和 css 加载器导入的css文件:
import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
  1. 设置构造函数并填入一些虚拟数据,以查看最终应用程序在填入来自智能合约的变量后的外观:
class Main extends React.Component {
    constructor() {
        super()

        this.state = {
            content: [{
                author: '0x211824098yf7320417812j1002341342342341234',
                message: 'This is a test',
                hashtags: ['test', 'dapp', 'blockchain'],
                time: new Date().toLocaleDateString(),
            }, {
                author: '0x211824098yf7320417812j1002341342342341234',
                message: 'This is another test',
                hashtags: ['sample', 'dapp', 'Ethereum'],
                time: new Date().toLocaleDateString(),
            }],
            topHashtags: ['dapp', 'Ethereum', 'blockchain', 'technology', 'design'],
            followedHashtags: ['electronics', 'design', 'robots', 'futurology', 'manufacturing'],
            displaySubscribe: false,
            displaySubscribeId: '',
        }
    }
  1. 创建带有ReactDOM渲染的render()函数:
    render() {
        return (
            <div className="main-container">
            </div>
        )
    }
}

ReactDOM.render(<Main />, document.querySelector('#root'))

如你所见,我们应用程序的状态包含了content对象,其中包含了一个以太坊地址作为该文章的作者,消息,标签和时间。我们以后可能会更改它,但现在已经足够好了。我们还添加了两个数组,其中包含了最热门的标签和这个特定用户关注的标签。这些显示订阅变量是为了在用户悬停在标签上时显示一个订阅按钮,以便他们有选择关注以改善 dApp 的互动性。

渲染标签

现在我们可以创建带有所有逻辑的渲染函数,但要警告你:由于我们要显示状态中的所有数组,所以有点复杂,请耐心地分段查看代码以理解。按照以下步骤进行:

  1. 创建一个新的函数来生成标签的 HTML,因为我们希望在按钮上添加可变逻辑,以确保hashtag文本对用户展示订阅或取消订阅按钮有反应。记住,我们希望用户能够关注标签;这就是我们需要订阅和取消订阅按钮的原因:
generateHashtags(hashtag, index) {
    let timeout
    return (
        <span onm ouseEnter={() => {
            clearTimeout(timeout)
            this.setState({
                displaySubscribe: true,
                displaySubscribeId: `subscribe-${hashtag}-${index}`,
            })
        }} onm ouseLeave={() => {
            timeout = setTimeout(() => {
                this.setState({
                    displaySubscribe: false,
                    displaySubscribeId: '',
                })
            }, 2e3)
        }}>
            <a className="hashtag" href="#">#{hashtag}</a>
            <span className="spacer"></span>
            <button ref={`subscribe-${hashtag}-${index}`} className={this.state.displaySubscribe && this.state.displaySubscribeId == `subscribe-${hashtag}-${index}` ? '' : 'hidden'} type="button">Subscribe</button>
            <span className="spacer"></span>
        </span>
    )
}
  1. 更新render()函数以生成内容和标签块,因为我们需要一种简单的方法来创建要显示的内容;所有逻辑将在render()函数中执行:
render() {
    let contentBlock = this.state.content.map((element, index) => (
        <div key={index} className="content">
            <div className="content-address">{element.author}</div>
            <div className="content-message">{element.message}</div>
            <div className="content-hashtags">{element.hashtags.map((hashtag, i) => (
                <span key={i}>
                    {this.generateHashtags(hashtag, index)}
                </span>
            ))}
            </div>
            <div className="content-time">{element.time}</div>
        </div>
    ))
  1. 添加标签块,其唯一工作是创建将显示给用户的 JSX 对象,使用我们刚刚使用的generateHashtags()函数:
let hashtagBlock = this.state.topHashtags.map((hashtag, index) => (
    <div key={index}>
        {this.generateHashtags(hashtag, index)}
    </div>
))
let followedHashtags = this.state.followedHashtags.map((hashtag, index) => (
    <div key={index}>
        {this.generateHashtags(hashtag, index)}
    </div>
))
  1. render()函数的末尾,添加带有我们刚刚设置的块变量的return块:
    return (
        <div className="main-container">
            <div className="hashtag-block">
                <h3>Top hashtags</h3>
                <div className="hashtag-container">{hashtagBlock}</div>
                <h3>Followed hashtags</h3>
                <div className="hashtag-container">{followedHashtags}</div>
            </div>
            <div className="content-block">
                <div className="input-container">
                    <textarea placeholder="Publish content..."></textarea>
                    <input type="text" placeholder="Hashtags separated by commas..."/>
                    <button type="button">Publish</button>
                </div>

                <div className="content-container">
                    {contentBlock}
                </div>
            </div>
        </div>
    )
}

我们添加了一个名为generateHashtags的函数,因为我们必须在许多地方添加相同的逻辑来显示订阅按钮,所以制作一个只在需要时执行此操作而不重复这些长代码块的函数是有意义的。然后,在render()函数中,您可以看到我们在许多地方使用该函数来生成标签逻辑。在返回之前,我们有三个变量,只是使用我们的状态数据动态生成 JSX 组件。最后,render()函数很好地显示了这些块。

改善外观

我还导入了index.css文件,其中包含用于以最佳方式显示我们的应用程序的网格组件,具有干净的结构,易于维护:

  1. 添加主要组件的一般样式到您的应用程序,例如 body 和按钮,以使它们看起来更好:
body {
    margin: 0;
    background-color: whitesmoke;
    font-family: sans-serif;
}

button {
    background-color: rgb(201, 47, 47);
    color: white;
    border-radius: 15px;
    border: none;
    cursor: pointer;
}

button:hover {
    background-color: rgb(131, 0, 0);
}
  1. 将一般隐藏和间隔样式添加到隐藏元素并创建动态间隔:
.hidden {
    display: none;
}

.spacer {
    margin-right: 5px;
}
  1. 将容器的样式添加到位置上,该位置现在在所有主要浏览器上都被接受的网格系统中:
.main-container {
    display: grid;
    grid-template-columns: 30% 70%;
    margin: auto;
    width: 50%;
    grid-column-gap: 10px;
}

.input-container {
    margin-bottom: 10px;
    padding: 30px;
    display: grid;
    grid-template-columns: 80% 1fr;
    grid-template-rows: 70% 30%;
    grid-gap: 10px;
}
  1. 格式化输入和文本区域,以创建一个外观更好,易于使用的设计:
.input-container textarea {
    padding: 10px;
    border-radius: 10px;
    font-size: 11pt;
    font-family: sans-serif;
    border: 1px solid grey;
    grid-column: 1 / 3;
}

.input-container input {
    padding: 10px;
    border-radius: 10px;
    font-size: 11pt;
    font-family: sans-serif;
    border: 1px solid grey;
}
  1. 为内容块提供一个看起来很棒的设计,类似于 Twitter 中的推文:
.content {
    background-color: white;
    border: 1px solid grey;
    margin-bottom: 10px;
    padding: 30px;
    box-shadow: 4px 4px 0px 0 #cecece;
}
.content-address {
    color: grey;
    margin-bottom: 5px;
}
.content-message {
    font-size: 16pt;
    margin-bottom: 5px;
}
.content-hashtags {
    margin-bottom: 5px;
}
.content-time {
    color: grey;
    font-size: 12pt;
}
  1. 格式化这些标签,以将它们放置在正确的位置,并增加它们的大小:
.hashtag-block {
    text-align: center;
}

.hashtag-container {
    line-height: 30px;
}

.hashtag {
    font-size: 15pt;
}
  1. 如果您希望实现相同的外观,可以复制并粘贴该 css。这是 dApp 目前的外观:

  1. 您可以在 GitHub 上查看完成的代码,网址是github.com/merlox/social-media-dapp/tree/master/src

我试图模拟一种简单的卡通设计,以使可视化更有趣,同时保持一个清晰的界面,人们可以轻松阅读而不会混淆。注意您创建的用户界面,因为它们是每个 dApp 的主要组成部分。外观专业的 dApp 会引起更多关注。更多的关注通常会转化为更多的收入,因为您能够在正确的时刻引导人们的注意力到正确的地方。

构建智能合约

我们要构建的智能合约将作为我们的分散式应用的后端,存储所有消息、主题标签和用户。在我们的应用中,我们希望保持用户的匿名性;这就是为什么他们被表示为地址而不是用户名——将人们的注意力集中在谈论的内容上,而不是消息的发布者。

正如您已经知道的,我们将创建一个以主题为中心的社交媒体平台,不包含图像或视频。这就是为什么我们所有的数据将存储在映射和数组的组合中。

规划设计流程

在直接进入代码之前,我想让您了解我们将遵循的优化整个流程、避免混乱并通过清晰地了解需要完成的工作来节省时间的过程。该过程如下所示:

  1. 创建一个智能合约文件,并在注释中写下合约的目的描述,例如函数的工作方式以及谁将使用它。尽量简洁,因为这将有助于您和维护人员了解它的全部内容。

  2. 开始创建变量和函数签名,即,没有主体的函数,只有名称和参数。使用 NatSpec 格式为每个函数编写文档以进行额外的澄清。

  3. 独立实现每个函数,直到所有函数都完成。如果需要,您可以添加更多函数。

  4. 通过将合约复制粘贴到 remix 或任何其他 IDE 中手动测试合约,以快速发现问题并在虚拟 EVM 中运行所有函数,在那里您无需支付任何 gas 费用或等待确认。理想情况下,您会编写 Truffle 测试来验证一切是否正常工作,但有时可以跳过以节省时间。

这是该流程的图形表示,以便您牢记:

这种类型的流程是我用来最大化生产力而不至于因规格而疯狂的流程。如果您立即开始编写解决方案,您可能会陷入一个需要重新制作整个代码库的地方,同时在此过程中创建不必要的错误。这就是为什么规划如此重要。此外,确切地知道该做什么以及何时做将使您的生活变得轻松得多。

现在我们可以开始创建我们的智能合约,描述其背后的想法。在您的contracts/文件夹中创建一个名为SocialMusic.sol的文件,并在文件顶部的注释中写下该合约最终版本应该包含的内容的描述。在查看我的解决方案之前,请尝试自己完成,因为学习的唯一方法就是自己练习:

// This is a social media smart contract that allows people to publish strings of text in short formats with a focus on hashtags so that they can follow, read and be in touch with the latest content regarding those hashtags. There will be a mapping of the top hashtags. A struct for each piece of content with the date, author, content and array of hashtags. We want to avoid focusing on specific users that's why user accounts will be anonymous where addresses will the be the only identifiers.

pragma solidity ⁰.5.5;

contract SocialMedia {}

无论您是否意识到,通过编写描述,您的思维都得到了很大程度的澄清。现在您可以开始创建函数和变量了。鉴于您已经有了用户界面,您将想将该界面分解为块,并创建将提供这些块中显示的数据的函数;例如,看一下您应用程序的以下块:

当你看着界面时,你显然能看到顶部的标签和一些随机的标签。当你看着界面时,你必须问自己,我需要在我的智能合约中实现什么来使这成为可能?嗯,这似乎显而易见,但往往并不那么容易。在这种情况下,你必须创建一个函数来检索顶部的标签。该函数将从排序数组或映射中获取数据并将其发送给用户,也许还有一个参数,用于确定在任何时刻要检索多少顶部标签,以便你可以尝试不同的数量。要创建该函数,你必须实现某种排序机制,可能是一个不消耗 gas 的纯函数或视图函数来进行处理。另一方面,你如何确定这些标签的顺序?可能是一个增加每个标签值的分数系统,具体取决于使用情况。

你看,从我们整个应用程序中一个小明显的部分,你意识到你需要以下内容:

  • 包含需要排序的顶部标签的数组或映射。

  • 一个用于检索那些标签的函数,还可以使用可选参数来确定要实验的标签数量。

  • 对现有标签进行排序的函数,考虑到区块链的限制,必须是一个纯函数或视图函数,以避免过高的 gas 成本。

  • 为每个标签分配一个分数的系统,这样我们可以根据它们的受欢迎程度对它们进行排序。

你必须为应用程序的每个组件进行相同的分析过程。无论它看起来多么显而易见,试着在脑海中描述这些部分,这样你就可以预先可视化所需和可能的内容,从而节省你数小时的沮丧和错误代码。

设置数据结构

在进行必要的规划之后,可以依次执行以下步骤为所有所需部分编写函数签名:

  1. 首先用结构体和事件定义以后要使用的变量:
struct Content {
    uint256 id;
    address author;
    uint256 date;
    string content;
    bytes32[] hashtags;
}

event ContentAdded(uint256 indexed id, address indexed author, uint256 indexed date, string content, bytes32[] hashtags);
  1. 添加映射、数组和剩余的状态变量:
mapping(address => bytes32[]) public subscribedHashtags;
mapping(bytes32 => uint256) public hashtagScore; // The number of times this hashtag has been used, used to sort the top hashtags
mapping(bytes32 => Content[]) public contentByHashtag;
mapping(uint256 => Content) public contentById;
mapping(bytes32 => bool) public doesHashtagExist;
mapping(address => bool) public doesUserExist;
address[] public users;
Content[] public contents;
bytes32[] public hashtags;
uint256 public latestContentId;
  1. 定义函数签名:
function addContent(string memory _content, bytes32[] memory _hashtags) public {}
function subscribeToHashtag(bytes32 _hashtag) public {}
function unsubscribeToHashtag(bytes32 _hashtag) public {}
function getTopHashtags(uint256 _amount) public view returns(bytes32[] memory) {}
function getFollowedHashtags() public view returns(bytes32[] memory) {}
function getContentIdsByHashtag(bytes32 _hashtag, uint256 _amount) public view returns(uint256[] memory) {}
function getContentById(uint256 _id) public view returns(uint256, address, uint256, string memory, bytes32[] memory) {}
function sortHashtagsByScore() public view returns(bytes32[] memory) {}
function checkExistingSubscription(bytes32 _hashtag) public view returns(bool) {}

你是否对我们在一瞬间想出的函数和变量的数量感到惊讶?在进行这个过程时,你可能没有考虑到像checkExistingSubscriptiongetContentIdsByHashtag这样的函数。老实说,我在编写合同之前并不知道这些函数是必需的;只是在创建整个代码之后,它们变得必要起来。如果你在创建代码之前没有想出所有必需的变量和函数,也没关系。它们将在适当的时刻浮出水面,当你开发时,你不必事先编写所有函数并计划每一个函数和变量;那将是疯狂的。所以要有耐心,并且知道,在实施你的初始函数之后,你可能需要添加一些额外的函数来实现所需的功能。

记录未来函数

那些功能还不够清晰,为什么不为它们中的每一个编写 NatSpec 文档?这是一个繁琐的过程,但在编码时会提醒您自己在做什么,所以您会感谢自己的。这是我的版本,包含了文档:

  1. 从添加内容、订阅和取消订阅函数开始:
/// @notice To add new content to the social media dApp. If no hashtags are sent, the content is added to the #general hashtag list.
/// @param _content The string of content
/// @param _hashtags The hashtags used for that piece of content
function addContent(string memory _content, bytes32[] memory _hashtags) public {}

/// @notice To subscribe to a hashtag if you didn't do so already
/// @param _hashtag The hashtag name
function subscribeToHashtag(bytes32 _hashtag) public {}

/// @notice To unsubscribe to a hashtag if you are subscribed otherwise it won't do nothing
/// @param _hashtag The hashtag name
function unsubscribeToHashtag(bytes32 _hashtag) public {}
  1. 用于顶部和已关注标签的获取器函数。我们需要这些函数将它们显示在用户界面的侧边栏上:
/// @notice To get the top hashtags
/// @param _amount How many top hashtags to get in order, for instance the top 20 hashtags
/// @return bytes32[] Returns the names of the hashtags
function getTopHashtags(uint256 _amount) public view returns(bytes32[] memory) {}

/// @notice To get the followed hashtag names for this msg.sender
/// @return bytes32[] The hashtags followed by this user
function getFollowedHashtags() public view returns(bytes32[] memory) {}
  1. 通过 ID 的获取器函数。我们需要它们将结构变量分解为单独的部分返回:
/// @notice To get the contents for a particular hashtag. It returns the ids because we can't return arrays of strings and we can't return structs so the user has to manually make a new request for each piece of content using the function below.
/// @param _hashtag The hashtag from which get content
/// @param _amount The quantity of contents to get for instance, 50 pieces of content for that hashtag
/// @return uint256[] Returns the ids of the contents so that you can get each piece independently with a new request since you can't return arrays of strings
function getContentIdsByHashtag(bytes32 _hashtag, uint256 _amount) public view returns(uint256[] memory) {}

/// @notice Returns the data for a particular content id
/// @param _id The id of the content
/// @return Returns the id, author, date, content and hashtags for that piece of content
function getContentById(uint256 _id) public view returns(uint256, address, uint256, string memory, bytes32[] memory) {}
  1. 辅助函数用于对标签进行排序和检查现有订阅情况。当用户订阅以更新整个标签的分数并根据分数排序它们时,将使用这些函数:
/// @notice Sorts the hashtags given their hashtag score
/// @return bytes32[] Returns the sorted array of hashtags
function sortHashtagsByScore() public view returns(bytes32[] memory) {}

/// @notice To check if the use is already subscribed to a hashtag
/// @return bool If you are subscribed to that hashtag or not
function checkExistingSubscription(bytes32 _hashtag) public view returns(bool) {}

NatSpec 文档描述了所有函数的基本描述、参数和其他程序员的返回值,以便他们可以维护您的代码。它们还帮助您理解代码基础增长时发生的情况。

接下来,我们必须逐一实现所有函数,直到所有函数都完成。这是最耗时的过程,因为考虑到 Solidity 的限制,一些部分比其他部分更难。在执行此操作之前,尽量保持积极。如果您设置了一个一到两小时的计时器,在完成之前不能分心,您会比预期完成得更早。这就是著名的番茄工作法,以最大程度地提高生产力,我建议您使用它以在较短时间内完成更多工作。

实现添加内容功能。

添加内容功能是我们正在构建的 dApp 中最复杂的,因为我们需要完成以下任务:

  1. 检查用户提供的内容是否有效。

  2. 将新内容添加到正确的状态变量中。

  3. 增加包含在内容片段中的标签的分数。

  4. 将内容动态存储在general标签中,人们可以使用它来查找未排序的随机内容。

  5. 如果是新客户,则将用户添加到用户数组中。

由于我们必须实现的函数很多,该函数不可避免地会很复杂。这就是为什么要花时间做好它很重要,因为我们很容易创建消耗所有可用燃气的燃气陷阱。在看到我的解决方案之前,请先去您的计算机上实施它们,尽量在自己的计算机上执行以下步骤:

  1. 添加require()检查以确保内容有效:
/// @notice To add new content to the social media dApp. If no hashtags are sent, the content is added to the #general hashtag list.
/// @param _content The string of content
/// @param _hashtags The hashtags used for that piece of content
function addContent(string memory _content, bytes32[] memory _hashtags) public {
    require(bytes(_content).length > 0, 'The content cannot be empty');
    Content memory newContent = Content(latestContentId, msg.sender, now, _content, _hashtags);
    // If the user didn't specify any hashtags add the content to the #general hashtag
  1. 根据用户是否添加了标签,我们将执行相应的功能来对这些标签进行排序并增加其值:
    if(_hashtags.length == 0) {
        contentByHashtag['general'].push(newContent);
        hashtagScore['general']++;
        if(!doesHashtagExist['general']) {
            hashtags.push('general');
            doesHashtagExist['general'] = true;
        }
    } else {
        for(uint256 i = 0; i < _hashtags.length; i++) {
            contentByHashtag[_hashtags[i]].push(newContent);
            hashtagScore[_hashtags[i]]++;
            if(!doesHashtagExist[_hashtags[i]]) {
                hashtags.push(_hashtags[i]);
                doesHashtagExist[_hashtags[i]] = true;
            }
        }
    }
  1. 使用前面描述的函数按分数对数组进行排序,并在创建用户时发出正确的事件:
    hashtags = sortHashtagsByScore();
    contentById[latestContentId] = newContent;
    contents.push(newContent);
    if(!doesUserExist[msg.sender]) {
        users.push(msg.sender);
        doesUserExist[msg.sender] = true;
    }
    emit ContentAdded(latestContentId, msg.sender, now, _content, _hashtags);
    latestContentId++;
}

这是我在那个函数中逐步完成的拆分:

  1. 我检查了包含消息的 _content 变量是否为空,方法是将其转换为字节并检查其长度。这是检查字符串是否为空的一种方法,因为无法获取字符串类型的长度。

  2. 我使用所需的参数创建了 Content 结构体实例,并开始填充使用该结构体的映射,以便稍后找到该内容。

  3. 用户可以选择不指定任何标签,此时内容将被添加到 #general 标签中,以某种方式为希望从应用程序获取一般信息的人组织起来。请记住,我们主要通过标签进行交互,因此将每条消息组织到一个标签中至关重要。

  4. 如果用户指定了一些标签,我们将内容添加到所有这些标签中,同时创建新的标签供人们关注。目前,我们对人们可以使用多少个标签没有任何限制,因为我们正在尝试应用程序的工作方式。如果我们决定设置此类限制,我们以后可以关注这些细节。

  5. 将用户添加到用户数组中,并发出 ContentAdded 事件,以通知其他人有关新内容的情况。

创建推广引擎

我们需要一种方法告诉用户哪些帐户表现最佳,方法是创建一个增加标签价值的评分系统。这就是我们创建 hashtagScore 映射的原因,作为衡量正在使用的标签受欢迎程度的方法。推广引擎只是一种按照受欢迎程度评分标签的方法。因此,当有人订阅该标签或为该标签添加新内容时,该标签的分数将会增加。当有人取消订阅时,分数将减少。这一切都是不可见的,所以用户只会看到热门标签。

让我们继续编写订阅函数,让人们有权关注他们感兴趣的特定主题。要实现推广引擎,我们只需在订阅和取消订阅函数中更新正在使用的特定标签的分数。再次强调,在看解决方案之前,尝试自己实现它,以锻炼你的技能并获取经验。以下是订阅函数,它增加了该特定用户选择的标签的分数:

/// @notice To subscribe to a hashtag if you didn't do so already
/// @param _hashtag The hashtag name
function subscribeToHashtag(bytes32 _hashtag) public {
    if(!checkExistingSubscription(_hashtag)) {
        subscribedHashtags[msg.sender].push(_hashtag);
        hashtagScore[_hashtag]++;
        hashtags = sortHashtagsByScore();
    }
}

然后我们有取消订阅函数,它减少了标签的价值,因为它变得不太相关:

/// @notice To unsubscribe to a hashtag if you are subscribed otherwise it won't do nothing
/// @param _hashtag The hashtag name
function unsubscribeToHashtag(bytes32 _hashtag) public {
    if(checkExistingSubscription(_hashtag)) {
        for(uint256 i = 0; i < subscribedHashtags[msg.sender].length; i++) {
            if(subscribedHashtags[msg.sender][i] == _hashtag) {
                delete subscribedHashtags[msg.sender][i];
                hashtagScore[_hashtag]--;
                hashtags = sortHashtagsByScore();
                break;
            }
        }
    }
}

subcribeToHashtag 函数简单地检查用户是否已订阅,以便将新主题添加到他们的兴趣列表中,同时对标签进行排序,因为该标签的分数已经增加。在我们的智能合约中,标签的价值取决于使用情况。订阅该标签的人越多,为该标签创建的内容越多,其排名就越高。

unsubscribeToHashtag函数循环遍历该特定用户的所有标签,并从其列表中移除选定的标签。此循环不应引起任何 gas 问题,因为我们不期望用户关注数十万个主题。无论如何,正确的做法是限制可订阅标签的数量,以避免 gas 错误。我会把这交给你。最后,我们降低该标签的评分,并对所有标签进行排序处理。

实现 getter 函数

接下来,让我们看看我们将用来向用户显示数据的 getter 函数。这些函数不需要任何 gas 费用,因为它们是从已下载和同步的区块链中读取数据,始终可用,而不依赖于互联网连接。让我们看看以下步骤:

  1. 创建getTopHashtags()函数,以 bytes32 格式返回用户可见的名称列表,以便查看哪些标签正在流行。这是发现新内容的主要系统:
/// @notice To get the top hashtags
/// @param _amount How many top hashtags to get in order, for instance the top 20 hashtags
/// @return bytes32[] Returns the names of the hashtags
function getTopHashtags(uint256 _amount) public view returns(bytes32[] memory) {
    bytes32[] memory result;
    if(hashtags.length < _amount) {
        result = new bytes32[](hashtags.length);
        for(uint256 i = 0; i < hashtags.length; i++) {
            result[i] = hashtags[i];
        }
    } else {
        result = new bytes32[](_amount);
        for(uint256 i = 0; i < _amount; i++) {
            result[i] = hashtags[i];
        }
    }
    return result;
}
  1. 添加获取已关注标签的函数,这很简单,因为它使用subscribedHashtags[]映射返回指定列表:
/// @notice To get the followed hashtag names for this msg.sender
/// @return bytes32[] The hashtags followed by this user
function getFollowedHashtags() public view returns(bytes32[] memory) {
    return subscribedHashtags[msg.sender];
}
  1. 实现getContentIdsByHashtag()函数。这将负责返回包含用户可能订阅的特定标签的所有内容片段的 ID 数组:
/// @notice To get the contents for a particular hashtag. It returns the ids because we can't return arrays of strings and we can't return structs so the user has to manually make a new request for each piece of content using the function below.
/// @param _hashtag The hashtag from which get content
/// @param _amount The quantity of contents to get for instance, 50 pieces of content for that hashtag
/// @return uint256[] Returns the ids of the contents so that you can get each piece independently with a new request since you can't return arrays of strings
function getContentIdsByHashtag(bytes32 _hashtag, uint256 _amount) public view returns(uint256[] memory) {
    uint256[] memory ids = new uint256[](_amount);
    for(uint256 i = 0; i < _amount; i++) {
        ids[i] = contentByHashtag[_hashtag][i].id;
    }
    return ids;
}
  1. 添加简单的getContentById()函数,用于将 ID 结构转换为可理解的单独变量,因为我们目前无法返回结构体:
/// @notice Returns the data for a particular content id
/// @param _id The id of the content
/// @return Returns the id, author, date, content and hashtags for that piece of content
function getContentById(uint256 _id) public view returns(uint256, address, uint256, string memory, bytes32[] memory) {
    Content memory c = contentById[_id];
    return (c.id, c.author, c.date, c.content, c.hashtags);
}

前面的函数相当简单。getContentIdsByHashtag 函数有点棘手,因为通常情况下我们不需要它,但由于 Solidity 不允许我们返回结构体数组或字符串数组,所以我们必须获得这些 ID,以便稍后可以使用getContentById函数逐个获取各个内容片段,该函数可以成功返回每个变量。

以下是我们需要使一切成为可能的最后两个辅助函数:

  • sortHashtagsByScore()函数用于返回按照每个标签的受欢迎程度排序的标签列表,因为我们正在读取每个标签的值:
/// @notice Sorts the hashtags given their hashtag score
/// @return bytes32[] Returns the sorted array of hashtags
function sortHashtagsByScore() public view returns(bytes32[] memory) {
    bytes32[] memory _hashtags = hashtags;
    bytes32[] memory sortedHashtags = new bytes32[](hashtags.length);
    uint256 lastId = 0;
    for(uint256 i = 0; i < _hashtags.length; i++) {
        for(uint j = i+1; j < _hashtags.length; j++) {
            // If it's a buy order, sort from lowest to highest since we want the lowest prices first
            if(hashtagScore[_hashtags[i]] < hashtagScore[_hashtags[j]]) {
                bytes32 temporaryhashtag = _hashtags[i];
                _hashtags[i] = _hashtags[j];
                _hashtags[j] = temporaryhashtag;
            }
        }
        sortedHashtags[lastId] = _hashtags[i];
        lastId++;
    }
    return sortedHashtags;
}
  • checkExistingSubscription()函数返回用户是否已订阅的布尔值:
/// @notice To check if the use is already subscribed to a hashtag
/// @return bool If you are subscribed to that hashtag or not
function checkExistingSubscription(bytes32 _hashtag) public view returns(bool) {
    for(uint256 i = 0; i < subscribedHashtags[msg.sender].length; i++) {
        if(subscribedHashtags[msg.sender][i] == _hashtag) return true;
    }
    return false;
}

排序函数因其明显的复杂性而难以阅读。尽管如此,它只是一对for循环,一个正常的循环和一个内部的倒序循环,连续将得分较高的标签移到顶部,直到最好的标签位于我们的sortedHashtags数组的第一个位置。这将用于替换过去的、未排序状态hashtags数组。

checkExistingSubscription 函数循环遍历所有已订阅的标签,并在提供的标签在列表中时返回true。这对订阅函数很重要,以保持数组清洁,避免重复订阅。

完整更新的代码可以在我的 GitHub 上查看,网址为github.com/merlox/social-media-dapp

现在剩下的是测试所有这些功能是否正常工作。将代码粘贴到 Remix 或任何其他 IDE 中,以便它指出必须修复的错误。然后将合同部署到 JavaScript VM 中,这不会产生任何费用,并逐一运行这些函数。注意,你将需要将bytes32变量转换为十六进制,如果你安装了 MetaMask,则可以在浏览器的开发者工具中使用web3.toHex()函数进行转换。

理想情况下,你可以在 Truffle 中编写测试,自动检查由新更改引起的错误。我会留下这个决定给你。

合约准备就绪后,下一步是在你的 dApp 中实施它,以便信息来自我们刚刚创建的去中心化后端。在下一节中看看如何实现它。

完成 dApp

你的 React.js Web 应用程序看起来很棒,剩下的就是连接智能合约到你的应用程序中的功能,以便它们互相交流,同时保持去中心化,因为任何人都可以在不依赖于集中式服务器的情况下自由使用 React 应用程序。

连接智能合约与 Web 应用程序的第一步是安装 web3.js,因为它是以太坊和 Web 浏览器之间的桥梁,尽管我们已经有了 MetaMask,你可能不需要它,但是重要的是选择一个稳定版本,不会为我们的 dApp 更改。请在项目文件夹中运行npm i -S web3

设置智能合约实例

在 React 应用程序中实施智能合约时,必须首先完成合约实例,以便我们可以在整个去中心化应用程序中调用该合约的方法。我们将使用 Truffle 提供的编译合约和其地址。让我们执行以下步骤:

  1. 将 web3 导入到你的项目中:
import Web3Js from 'web3'

你觉得为什么我将变量命名为 Web3Js 而不直接用 Web3 呢?因为 MetaMask 注入了自己版本的 web3,准确地命名为 Web3,因此当我们开发时,可能会使用 MetaMask 注入的 web3 版本,而不是我们想要导入的版本。为了避免与 MetaMask 注入的 web3 发生干扰,重要的是使用略微不同的名称。

  1. 全局使用当前提供程序设置 web3,这样你就可以在整个应用程序中使用它,而不必担心范围问题。

  2. 创建一个名为setup()的函数,其中包含 MetaMask 设置逻辑。这个函数将在构造函数中执行,页面加载时执行:

class Main extends React.Component {
    constructor() {
        // Previous code omitted for simplicity

        this.setup()
    }

 async setup() {
 window.web3js = new Web3Js(ethereum)
 try {
 await ethereum.enable();
 } catch (error) {
 alert('You must approve this dApp to interact with it, reload it to approve it')
 }
 }
}

我们创建了一个新的设置函数,因为我们无法在构造函数上使用 await,因为它不是一个异步函数。在其中,我们创建了一个名为web3js的全局变量,该变量不叫做web3(小写),因为 MetaMask 已经使用了该变量名,我们有可能使用错误的版本。如您所见,在本例中,提供程序称为ethereum,这是来自 MetaMask 的全局变量,其中包含了我们启动使用 web3 所需的一切;这是一种新的初始化 web3 实例的方式,与旧版 dApp 兼容,因为 MetaMask 团队对安全性进行了一些更改。然后我们等待enable()函数获得用户的许可以注入 web3,因为我们不希望在没有用户同意的情况下暴露用户密钥。如果用户不允许,我们会显示一个错误,让他们知道我们需要他们授予权限以使此 dApp 正常工作。

  1. 设置智能合约实例。因为我们已经安装了 Truffle,我们可以编译我们的智能合约以生成包含 ABI 的 JSON 文件,该文件是使用该应用程序所必需的。然后我们可以将合约部署到ropsten
truffle compile

truffle deploy --network ropsten --reset

您可能会收到以下消息:

"Unknown network "ropsten". See your Truffle configuration file for available networks."
  1. 这意味着您没有正确设置 Truffle 配置文件以使用ropsten网络。使用npm i -S truffle-hdwallet-provider安装钱包提供程序。然后使用以下代码修改truffle-config.js
const HDWalletProvider = require('truffle-hdwallet-provider')
const infuraKey = "https://ropsten.infura.io/v3/8e12dd4433454738a522d9ea7ffcf2cc"

const fs = require('fs')
const mnemonic = fs.readFileSync(".secret").toString().trim()

module.exports = {
  networks: {
    ropsten: {
      provider: () => new HDWalletProvider(mnemonic, infuraKey),
      network_id: 3, // Ropsten's id
      gas: 5500000, // Ropsten has a lower block limit than mainnet
      confirmations: 2, // # of confs to wait between deployments. (default: 0)
      timeoutBlocks: 200, // # of blocks before a deployment times out (minimum/default: 50)
      skipDryRun: true // Skip dry run before migrations? (default: false for public nets )
    }
  }
}
  1. 告诉 Truffle 通过在migrations/文件夹中创建一个名为2_deploy_contract.js的文件来部署您的合约,其中包含以下代码:
const SocialMedia = artifacts.require("./SocialMedia.sol")

module.exports = function(deployer) {
  deployer.deploy(SocialMedia);
}
  1. 如您所见,我们只有最小的配置参数,因此保持它简洁。在您的项目文件夹中创建一个.secret文件,并粘贴您的以太坊种子短语,您可以通过重置 MetaMask 或在另一个浏览器中安装它来获取该种子短语,如果您担心使您的种子公开。该种子短语将被 Truffle 用于部署合约,所以请确保您的第一个账户中有足够的ropsten以太币。然后再次运行truffle deploy --network ropsten --reset

  2. 使用以下内容更新您的setup函数以创建合约实例:

async setup() {
    window.web3js = new Web3Js(ethereum)
    try {
        await ethereum.enable();
    } catch (error) {
        alert('You must approve this dApp to interact with it, reload it to approve it')
    }
 const user = (await web3js.eth.getAccounts())[0]
 const contract = new web3js.eth.Contract(ABI.abi, ABI.networks['3'].address, {
 from: user
 })
 await this.setState({contract, user})
}

我们已经设置了应用程序状态中的用户账户,以便在需要时轻松访问它。

将您的数据去中心化

要完全实现智能合约,我们必须查看网站的每个部分,以使用智能合约中的数据更新其内容。让我们从左上角到右下角进行。按照顺序,我们必须首先去中心化顶部的标签部分,使用getTopHashtags()函数:

async setup() {
    window.web3js = new Web3Js(ethereum)
    try {
        await ethereum.enable();
    } catch (error) {
        alert('You must approve this dApp to interact with it, reload it to approve it')
    }
    const user = (await web3js.eth.getAccounts())[0]
    window.contract = new web3js.eth.Contract(ABI.abi, ABI.networks['3'].address, {
        from: user
    })
 await this.setState({contract, user})
}

当您没有任何热门标签时,您还必须更新您的render()函数,因为您刚刚部署了智能合约。我们将从另一个名为getContent()的函数中获取内容:

render() {
    return (
        <div className="main-container">
            <div className="hashtag-block">
                <h3>Top hashtags</h3>
                <div className="hashtag-container">{this.state.topHashtagBlock}</div>
                <h3>Followed hashtags</h3>
                <div className="hashtag-container">{this.state.followedHashtagsBlock}</div>
            </div>
            <div className="content-block">
                <div className="input-container">
                    <textarea ref="content" placeholder="Publish content..."></textarea>
                    <input ref="hashtags" type="text" placeholder="Hashtags separated by commas without the # sign..."/>
                    <button onClick={() => {
                        this.publishContent(this.refs.content.value, this.refs.hashtags.value)
                    }} type="button">Publish</button>
                </div>

                <div className="content-container">
                    {this.state.contentsBlock}
                </div>
            </div>
        </div>
    )
}

修改后的代码如下所示:

让我们更新获取内容函数,以根据用户是否有任何活动订阅来生成数据:

  1. 要获取用户将要看到的所有内容,我们需要获取 latestContentId,这是一个表示当前时刻可用多少个内容片段的数字,以防用户尚未订阅任何标签:
async getContent() {
    const latestContentId = await this.state.contract.methods.latestContentId().call()
    const amount = 10
    const amountPerHashtag = 3
    let contents = []
    let counter = amount
  1. 如果用户正在关注标签,则通过循环遍历所有 ID 获取内容片段:
    // If we have subscriptions, get content for those subscriptions 3 pieces per hashtag
    if(this.state.followedHashtags.length > 0) {
        for(let i = 0; i < this.state.followedHashtags.length; i++) {
            // Get 3 contents per hashtag
            let contentIds = await this.state.contract.methods.getContentIdsByHashtag(this.bytes32(this.state.followedHashtags[i]), 3).call()
            let counterTwo = amountPerHashtag
            if(contentIds < amountPerHashtag) counterTwo = contentIds
            for(let a = counterTwo - 1; a >= 0; a--) {
                let content = await this.state.contract.methods.getContentById(i).call()
                content = {
                    id: content[0],
                    author: content[1],
                    time: new Date(parseInt(content[2] + '000')).toLocaleDateString(),
                    message: content[3],
                    hashtags: content[4],
                }
                content.message = web3js.utils.toUtf8(content.message)
                content.hashtags = content.hashtags.map(hashtag => web3js.utils.toUtf8(hashtag))
                contents.push(content)
            }
        }
    }
  1. 如果用户尚未订阅任何标签,则更新 counter 变量以反向循环,以便首先获取最新的内容片段:
    // If we don't have enough content yet, show whats in there
    if(latestContentId < amount) counter = latestContentId
    for(let i = counter - 1; i >= 0; i--) {
        let content = await this.state.contract.methods.getContentById(i).call()
        content = {
            id: content[0],
            author: content[1],
            time: new Date(parseInt(content[2] + '000')).toLocaleDateString(),
            message: content[3],
            hashtags: content[4],
        }
        content.message = web3js.utils.toUtf8(content.message)
        content.hashtags = content.hashtags.map(hashtag => web3js.utils.toUtf8(hashtag))
        contents.push(content)
    }
  1. 生成 contentsBlock,其中包含创建内容片段的所有元素,类似于推特或 Facebook 的帖子:
    let contentsBlock = await Promise.all(contents.map(async (element, index) => (
        <div key={index} className="content">
            <div className="content-address">{element.author}</div>
            <div className="content-message">{element.message}</div>
            <div className="content-hashtags">{element.hashtags.map((hashtag, i) => (
                <span key={i}>
                    <Hashtag
                        hashtag={hashtag}
                        contract={this.state.contract}
                        subscribe={hashtag => this.subscribe(hashtag)}
                        unsubscribe={hashtag => this.unsubscribe(hashtag)}
                    />
                </span>
            ))}
            </div>
            <div className="content-time">{element.time}</div>
        </div>
    )))

    this.setState({contentsBlock})
}

getContent() 函数检查用户是否有任何活动订阅,以便它可以获取每个标签最多三个内容片段。它还将获取 dApp 上载的最近 10 篇文章。它相当庞大,因为它根据智能合约上可用的标签数量生成数据。如果您关注 100 个标签,您将看到 300 个新的内容片段,因为我们在 feed 中每个标签获取 3 篇文章。我们还将添加 10 个随机内容,这些内容将从智能合约中的 contents 数组中取出。

创建标签组件

每个标签都是一个小型机器,包含了大量的逻辑来检测用户是否已订阅。这可能看起来很简单,但请记住,我们需要获取每个用户对每个标签的状态,这意味着我们必须执行大量请求,可能会减慢我们的 dApp 的性能。创建函数时要保持清洁,以便它们运行顺畅。

我们正在使用一个名为 Hashtag 的新组件,它是一个 HTML 对象,返回一个交互式的标签文本,可以点击进行订阅或取消订阅。这是创建这种功能的最简洁方式,以减少复杂性:

  1. 创建构造函数,带有一些状态变量,根据用户的行为显示或隐藏标签:
class Hashtag extends React.Component {
 constructor(props) {
        super()
        this.state = {
            displaySubscribe: false,
            displayUnsubscribe: false,
            checkSubscription: false,
            isSubscribed: false,
        }
 }
  1. 创建 bytes32()checkExistingSubscription() 函数来检查当前用户是否已经关注了特定的标签:
 componentDidMount() {
        this.checkExistingSubscription()
 }

 bytes32(name) {
        let nameHex = web3js.utils.toHex(name)
        for(let i = nameHex.length; i < 66; i++) {
            nameHex = nameHex + '0'
        }
        return nameHex
 }

 async checkExistingSubscription() {
        const isSubscribed = await this.props.contract.methods.checkExistingSubscription(this.bytes32(this.props.hashtag)).call()
        this.setState({isSubscribed})
    }
  1. render() 函数相当庞大,因此我们将其分解为两个主要部分:检测用户是否已订阅的功能,以及显示正确按钮的功能:
 render() {
        return (
            <span onm ouseEnter={async () => {
                if(this.state.checkSubscription) await this.checkExistingSubscription()
                if(!this.state.isSubscribed) {
                    this.setState({
                        displaySubscribe: true,
                        displayUnsubscribe: false,
                    })
                } else {
                    this.setState({
                        displaySubscribe: false,
                        displayUnsubscribe: true,
                    })
                }
            }} onm ouseLeave={() => {
                this.setState({
                    displaySubscribe: false,
                    displayUnsubscribe: false,
                })
            }}>
  1. 实现订阅或取消订阅按钮,当用户悬停在标签上时显示:
                <a className="hashtag" href="#">#{this.props.hashtag}</a>
                <span className="spacer"></span>
                <button onClick={() => {
                    this.props.subscribe(this.props.hashtag)
                    this.setState({checkSubscription: true})
                }} className={this.state.displaySubscribe ? '' : 'hidden'} type="button">Subscribe</button>
                <button onClick={() => {
                    this.props.unsubscribe(this.props.hashtag)
                    this.setState({checkSubscription: true})
                }} className={this.state.displayUnsubscribe ? '' : 'hidden'} type="button">Unsubscribe</button>
                <span className="spacer"></span>
            </span>
 )
 }
}

render() 函数显示标签,当鼠标悬停时显示订阅或取消订阅按钮。checkExistingSubscription() 函数获取特定标签订阅的状态,以显示适合取消订阅的用户的按钮类型。

创建标签获取器

当页面加载时,我们现在可以创建一个函数,从智能合约中获取顶级标签和已关注的标签。我们将通过检索已关注的和顶级标签来完成。这些标签将通过循环遍历它们直到界面填满数据,显示给用户。

一旦完成,请尝试自行实现并查看以下结果:

  1. 定义创建结果标签 JSX 所需的变量:
async getHashtags() {
    let topHashtagBlock
    let followedHashtagsBlock
    const amount = 10
    const topHashtags = (await contract.methods.getTopHashtags(amount).call()).map(element => web3js.utils.toUtf8(element))
    const followedHashtags = (await this.state.contract.methods.getFollowedHashtags().call()).map(element => web3js.utils.toUtf8(element))
  1. 开始循环遍历标签块,直到我们填满顶部标签列表:
    if(topHashtags.length == 0) {
        topHashtagBlock = 'There are no hashtags yet, come back later!'
    } else {
        topHashtagBlock = topHashtags.map((hashtag, index) => (
            <div key={index}>
                <Hashtag
                    hashtag={hashtag}
                    contract={this.state.contract}
                    subscribe={hashtag => this.subscribe(hashtag)}
                    unsubscribe={hashtag => this.unsubscribe(hashtag)}
                />
            </div>
        ))
    }
  1. 如果用户没有关注任何标签,我们将显示一条消息。如果他们有,我们将循环遍历所有关注的标签,生成具有所需数据的 Hashtag 组件。使用刚刚创建的新块更新状态,以便在 render() 函数中显示它们:
    if(followedHashtags.length == 0) {
        followedHashtagsBlock = "You're not following any hashtags yet"
    } else {
        followedHashtagsBlock = followedHashtags.map((hashtag, index) => (
            <div key={index}>
                <Hashtag
                    hashtag={hashtag}
                    contract={this.state.contract}
                    subscribe={hashtag => this.subscribe(hashtag)}
                    unsubscribe={hashtag => this.unsubscribe(hashtag)}
                />
            </div>
        ))
    }
    this.setState({topHashtagBlock, followedHashtagsBlock, followedHashtags})
}

创建发布功能

发布新的内容是一个简单的任务,需要验证所有输入是否包含有效的文本字符串。由于我们将标签存储在一个 bytes32 变量中,所以需要正确格式化用户输入的标签,以便智能合约能够安全处理它们。

让我们让发布功能起作用,这样我们就可以开始生成内容,执行以下步骤:

  1. 如果尚未这样做,请创建 bytes32() 函数,因为我们很快会需要它:
bytes32(name) {
    let nameHex = web3js.utils.toHex(name)
    for(let i = nameHex.length; i < 66; i++) 
    {
        nameHex = nameHex + '0'
    }
    return nameHex
}
  1. 添加 publishContent() 函数来处理带有标签的消息。标签将以字符串格式给出,其中包含不带散列符号(#)的逗号分隔字符串列表。确保标签被正确分隔和格式化,以供合约安全处理:
async publishContent(message, hashtags) {
    if(message.length == 0) alert('You must write a message')
    hashtags = hashtags.trim().replace(/#*/g, '').replace(/,+/g, ',').split(',').map(element => this.bytes32(element.trim()))
    message = this.bytes32(message)
    try {
        await this.state.contract.methods.addContent(message, hashtags).send({
            from: this.state.user,
            gas: 8e6
        })
    } catch (e) {console.log('Error', e)}
    await this.getHashtags()
    await this.getContent()
}

这是我们刚刚添加的两个函数的解释:

  • bytes32(): 这个函数用于将普通字符串转换为 Solidity 可用的十六进制,因为新的更新强制 web3 用户在处理 bytes 类型的变量时将数据转换为十六进制。

  • publishContent(): 这个函数看起来有点凌乱,因为我们正在使用正则表达式将用户输入的标签转换为有效的清晰字符串数组。它正在执行一些操作,比如删除空格、删除重复逗号和标签符号,然后将字符串拆分成一个有效的数组,可以在我们的智能合约中使用。

  1. 记得更新你的 setup() 函数,以便在加载时获取最新的内容:
async setup() {
    window.web3js = new Web3Js(ethereum)
    try {
        await ethereum.enable();
    } catch (error) {
        alert('You must approve this dApp to interact with it, reload it to approve it')
    }
    const user = (await web3js.eth.getAccounts())[0]
    window.contract = new web3js.eth.Contract(ABI.abi, ABI.networks['3'].address, {
        from: user
    })
    await this.setState({contract, user})
 await this.getHashtags()
 await this.getContent()
}
  1. 是时候专注于创建订阅功能了。它们将在用户点击订阅或取消订阅时执行,取决于当前状态。尝试自己实现它们,然后一旦完成,请返回比较你的解决方案和我的。记住,这是尝试和失败,直到代码变得足够好的过程。这是我的解决方案:
async subscribe(hashtag) {
    try {
        await this.state.contract.methods.subscribeToHashtag(this.bytes32(hashtag)).send({from: this.state.user})
    } catch(e) { console.log(e) }
    await this.getHashtags()
    await this.getContent()
}

async unsubscribe(hashtag) {
    try {
        await this.state.contract.methods.unsubscribeToHashtag(this.bytes32(hashtag)).send({from: this.state.user})
    } catch(e) { console.log(e) }
    await this.getHashtags()
    await this.getContent()
}

这两个函数都相当简单。当用户按下标签名称旁边的按钮时,它们运行适当的订阅或取消订阅函数。注意我们如何使用 try catch 避免在调用合约时出现故障时破坏整个应用程序;这也是因为有时它有一个奇怪的故障系统,在没有原因的情况下停止执行。当你觉得需要时,只需添加 try catch 块。

你可以在 GitHub 上找到更新版本,网址为 github.com/merlox/social-media-dapp,其中包含完整的实现代码供您参考。就是这样!现在你的区块链开发简历上有了一个新项目,你可以向雇主展示,或者在此基础上构建一个更好的去中心化社交媒体平台来筹集资金。

总结

当涉及到为用户自由发布内容创建一个完全去中心化的社交媒体平台时,就是这些了。在本章中,您了解了在区块链上创建这种类型的应用程序与在集中式系统上创建它之间的优势。然后,您通过使用 Truffle 和 React 从头开始设置了用户界面。之后,您开发了智能合约并将其连接到 dApp 以使其交互式。总的来说,您获得了一大块经验,可以将其扩展为创建具有各种有趣功能的不同类型的社交媒体平台,例如关注用户和添加用于与不同 API 交互的 Oracle。

在下一章中,我们将探讨在区块链上构建去中心化电子商务市场的构建过程,您将为您的业务创建一个完全功能的商店。

第十二章:创建基于区块链的电子商务市场

区块链技术最佳应用案例之一是分散的电子商务市场,原因很简单,你不必支付费用,也不必把数据委托给那些会为了利润出售数据的强大企业。Ethereum 为此提供了一个出色的解决方案,新的 ERC-721 代币标准已经为您在区块链上生成数字化物体。在本章中,你将学习如何处理个人用户数据,以便为每个个体保护数据,鉴于 Ethereum 是一个公共系统。

在第一部分中,我们将研究电子商务网站应该如何构建,使用户可以像在真实商店一样与之互动。你将构建用户界面,用于显示使用 ERC-721 约定标识的独特产品。然后,你将实现 React 路由器模块,以在用户友好的界面中组织不同的视图。最后,你将创建实施 ERC-721 代币并创建管理分散式产品所需功能的智能合约。

此外,在本章中,你将学习如何在 Ethereum 上为您的企业创建一个完整的电子商务市场,学习以下主题:

  • 创建用户界面

  • 理解 ERC-721 代币

  • 开发电子商务智能合约

  • 完成 dApp

创建用户界面

这类指南的最大优点是,你可以将在这里学到的关于分散电子商务的知识应用到扩展这些想法上,创造一个更高级的产品,提供一个复杂的解决方案以募集资金,或者简单地以此来建立一个业务。

计划市场界面

该市场几乎拥有无限的选择,因为你不必面对许多区块链的限制。每个产品都是一个独立的实例,可以根据需要进行修改,因此你可以自由添加尽可能多的功能,比如以下功能:

  • 将产品加入购物车的购物系统,从而实现较大的综合采购,而不是直接购买

  • 动态的发货地址功能,以添加多个不同的地址,以便您可以通过保存您的首选位置快速向多个地点发送订单

  • 创建用于用户产品拍卖的竞标系统

  • 为更好的用户互动而创建的个人资料和评价功能

在这个项目中,我们不会实现任何那些高级功能,因为它们会花费太多时间来开发,尽管你可以在基本产品完成后自己添加它们。这就是为什么我们将创建一个具有以下功能的简单接口:

  • 通过 Ethereum 直接购买实物和数字产品的购买系统

  • 作为独立卖家,在市场上发布产品的销售功能

  • 作为买家和卖家查看待处理订单的订单展示功能

通常情况下,用户将能够像使用信用卡一样作为普通在线商店与 MetaMask 进行直接付款交互。与像亚马逊这样收取约 15%总付款费用的电子商务商店相比,该市场不会向用户收取费用,这真的很费钱。另一个重要的点是,不会有任何审查或需要遵循的规则,这意味着用户可以自由发布产品,而不必担心被来自中心化实体的禁令所影响,这是一个经常发生的问题,导致卖家损失了数千美元的锁定资金和撤销的订单。

不会有多个数量的单个产品,因为我们将使用独特的不可替代令牌NFT),这意味着每个产品都必须是唯一的。由于我们将从一个用户向另一个用户交换令牌,所以我们将无法拥有同一产品的多个副本。然而,你可以实现一个 ERC-20 代币或一个系统,用于为同一产品的多个数量生成相同的令牌 ID 的多个副本。

让我们首先通过克隆基础存储库(github.com/merlox/dapp)或自行配置npm和 Truffle 来设置项目。在设置 Truffle 或克隆存储库后,你应该有以下文件夹和初始文件:

  • contracts/

  • dist/

  • migrations/

  • node_modules/(在克隆存储库后记得使用npm install

  • src/

    • index.js

    • index.htmlindex.ejs,根据你的喜好

    • index.cssindex.styl,根据你的喜好

  • .babelrc

  • .gitignore

  • LICENSE

  • package.json

  • README.md

  • truffle-config.js

  • webpack.config.js(记得设置好你的 webpack 配置)

在你的src/文件夹内,创建一个名为components/的新文件夹,其中将包含每个 JavaScript 组件的文件,因为这是一个较大的 dApp,我们将有许多不同的组件。因为我们将有多个页面,我们希望使用 React 路由器来管理历史位置和 URL,以便用户可以在页面之间进行导航。通过在终端上运行以下命令来安装 React 路由器和web3库:

npm i -S web3 react-router-dom

设置索引页面

打开你的index.js文件,导入所需的库,并使用一些虚拟数据设置初始状态,以查看最终设计的外观。我们通过以下步骤来实现这一点:

  1. 导入所需的库。我们需要来自react-router库的几个组件,如下所示的代码:
import React from 'react'
import ReactDOM from 'react-dom'
import MyWeb3 from 'web3'
import { BrowserRouter, Route, withRouter } from 'react-router-dom'
  1. 创建构造函数,并添加一些具有必要属性的产品,以尽可能多地向用户显示信息,如下所示的代码。标题、描述、ID 和价格等属性是必须的:
class Main extends React.Component {
    constructor(props) {
        super(props)

        this.state = {
            products: [{
                id: 1,
                title: 'Clasic trendy shoes',
                description: 'New unique shoes for sale',
                date: Date.now(),
                owner: '',
                price: 12,
                image: 'https://cdn.shopify.com/s/files/1/2494/8702/products/Bjakin-2018-Socks-Running-Shoes-for-Men-Lightweight-Sports-Sneakers-Colors-Man-Sock-Walking-Shoes-Big_17fa0d5b-d9d9-46a0-bdea-ac2dc17474ce_400x.jpg?v=1537755930'
            }
            productsHtml: [],
            productDetails: [],
            product: {},
        }
    }
  1. 您可以通过复制 product 对象并更改一些参数使其看起来独特来添加更多产品。然后添加 bytes32() 函数将字符串转换为有效的十六进制以及 render() 函数,如下所示:
    bytes32(name) {
        return myWeb3.utils.fromAscii(name)
    }

    render() {
        return (
            <div>
                <Route path="/" exact render={() => (
                    <div>The dApp has been setup</div>
                )} /> 
            </div>
        )
    }
}
  1. 使用 React 路由器提供的 withRouter() 函数为我们的 Main 组件提供历史属性,这对于在您的 dApp 中在页面之间导航是必要的。如下所示:
// To be able to access the history in order to redirect users programmatically when opening a product
Main = withRouter(Main)
  1. 添加来自 React 路由器的 BrowserRouter 组件以初始化路由器对象,如下所示:
ReactDOM.render(
    <BrowserRouter>
        <Main />
    </BrowserRouter>,
document.querySelector('#root'))

BrowserRouter 组件是用于初始化路由器的主要组件,以便它们可以管理不同的页面。我们使用 withRouter 导入来访问导航历史,以便我们可以编程方式更改页面。基本上,我们需要它在我们的 dApp 中在特定时间重定向用户到不同页面。然后我们在 this.state 对象中设置一些基本产品与不同的属性。注意图片是一个 URL 而不是文件。由于我们没有处理文件的服务器,我们需要卖家在某种公共服务上托管自己的图片,比如 Imgur。

React 路由库将使用多个 Route 实例来确定在什么时间加载哪个页面。我们还必须在我们的 Main 组件顶部添加高级 BrowserRouter 组件来激活路由器。注意我们如何使用 exact path="/" 渲染单个路由,显示设置文本以确认应用程序配置成功加载后。

配置 webpack 开发服务器

创建 Main 组件后,您将想要运行应用程序以查看其外观,但在这种情况下,我们将使用 webpack-dev-server 扩展,该扩展会在我们开发时自动重新加载网站,以便我们不必不断手动重新加载它并在后端编译文件。因此,而不是设置 webpack 观察者和静态服务器,它全部包含在一个单独的命令中。使用以下命令在本地安装 webpack 服务器:

npm i -S webpack-dev-server

然后在 package.json 文件的 scripts 部分下更新新的脚本(如下所示);否则,它将无法工作,因为我们需要从项目内部执行此命令:

{
  "name": "dapp",
  "version": "1.0.0",
  "description": "",
  "main": "truffle-config.js",
  "directories": {
    "test": "test"
  },
 "scripts": {
 "dev": "webpack-dev-server -d"
 }
}

这只是使用 -d 标志运行 webpack-dev-server 命令,该标志将模式设置为开发模式,允许您从未压缩的文件中看到完整的错误消息。如果愿意,可以添加 -o 标志,在运行命令时打开浏览器。通过运行以下命令行来执行它:

npm run dev

如果一切正确,您将能够访问 localhost:8080 并看到已设置路由器的页面。

创建头部组件

我们的应用程序将有几个页面用于买家、卖家和订单。这就是为什么尽可能将每个组件尽可能地分离成可以在需要时导入的唯一块的重要性,通过执行以下步骤来完成:

  1. src/components/ 文件夹内创建一个新的组件,用于显示我们网站的标题,并创建一个名为 Header.js 的文件,放在你的 components 文件夹内,如下面的代码所示:
import React from 'react'
import { Link } from 'react-router-dom'

function Header() {
    return (
        <div className="header">
            <Link to="/">ECOMMERCE</Link>
            <div>
                <Link to="/">Home</Link>
                <Link to="/sell">Sell</Link>
                <Link to="/orders">Orders</Link>
            </div>
        </div>
    )
}

export default Header
  1. 使用 export default Header 导出,以便其他文件可以访问你的组件。然后按照下面的代码将其导入到你的 index.js 页面中,以在导入的库下方显示它,以保持它们的顺序:
import React from 'react'
import ReactDOM from 'react-dom'
import MyWeb3 from 'web3'
import { BrowserRouter, Route, withRouter } from 'react-router-dom'
import Header from './components/Header'
  1. 使用组件实例更新你的 render() 函数,如下面的代码所示:
render() {
    return (
        <div>
            <Route path="/" exact render={() => (
                <Header />
            )} />
        </div>
    )
}

你将看到你的标题会自动加载,无需刷新你的 webpack 服务,如下面的截图所示:

  1. 目前看起来不太好,所以让我们用一些 stylus CSS 来改进设计。如果你还没有配置它,请使用以下命令安装 stylusstylus-loader 库:
npm i -S stylus stylus-loader
  1. 根据以下内容更新你的 webpack 配置:
require('babel-polyfill')
const webpack = require('webpack')
const html = require('html-webpack-plugin')
const path = require('path')

module.exports = {
    entry: ['babel-polyfill', './src/index.js'],
    output: {
        filename: 'bundle.js',
        path: path.join(__dirname, 'dist')
    },
    module: {
        rules: [
            {
                test: /\.js$/,
                exclude: /node_modules/,
                use: {
                    loader: 'babel-loader'
                }
            }, {
 test: /\.styl$/,
 exclude: /node_modules/,
 use: [
 {loader: 'style-loader'},
 {loader: 'css-loader'},
 {loader: 'stylus-loader'}
 ]
 }
        ]
    },
    plugins: [
        new webpack.HotModuleReplacementPlugin(),
        new html({
            title: "dApp project",
            template: './src/index.ejs',
            hash: true
        })
    ]
}

这里是我们在 webpack 文件中进行的主要更改:

  • 我们导入 webpack,以便我们可以使用 webpack.HotModuleReplacementPlugin() 在进行更改时部分重新加载页面。而不是重新加载整个页面,只会重新加载更改的组件。

  • 然后我们设置 stylus 加载器来加载 styl 文件。

  1. 创建 index.styl,采用以下设计,尽管最终你的电子商务商店的外观是由你决定的:
productPadding = 20px

body
    background-color: whitesmoke
    font-family: sans-serif
    margin: 0

button
    border: none
    background-color: black
    color: white
    cursor: pointer
    padding: 10px
    width: 200px
    height: 50px

    &:hover
        opacity: 0.9

input, textarea
    padding: 20px
    border: 1px solid black

.header
    background-color: black
    color: white
    padding: 15px
    margin-bottom: 20px
    text-align: center
    display: flex
    justify-content: space-around

    a
        color: white
        text-decoration: none
        margin-right: 10px

        &:hover
            color: lightgrey
  1. 注意顶部的 productPadding 变量。Stylus 允许我们创建变量,以便我们可以轻松地在样式文件中配置相同值的多个实例;我们稍后会使用该变量。然后像这样在你的 index.js 文件中导入 stylus 文件:
import './index.styl'

现在检查你的应用在浏览器中的外观;由于你更新了 webpack 配置,你可能需要重新加载你的 webpack 服务器:

创建主页组件

Home 组件将包含显示用户首次打开 dApp 时看到的第一个页面的逻辑,以便他们可以开始购买产品。该组件将是管理剩余页面的核心组件。

创建一个默认设计的 Home 组件,用于主页;它将包含一个干净设计的最新产品。以下是位于组件文件夹中的 Home.js 文件的代码:

import React from 'react'
import MyWeb3 from 'web3'
import Header from './Header'

class Home extends React.Component {
    constructor() { super() }
    render() {
        return (
            <div>
                <Header />
                <div className="products-container">{this.props.productsHtml}</div>
                <div className="spacer"></div>
            </div>
        )
    }
}

export default Home

你可以将其导入到你的 index.js 文件中,这将是主要的数据和功能来源。同时,从索引中删除 Header 的导入,因为它已经包含在 Home 组件中了。下面的步骤显示了你需要做的更改,以便将 Home 组件包含在你的 dApp 中:

  1. 在文件开头导入组件,同时移除Header组件,因为我们已经在Home组件中包含了它:
import React from 'react'
import ReactDOM from 'react-dom'
import MyWeb3 from 'web3'
import { BrowserRouter, Route, withRouter } from 'react-router-dom'
import Home from './components/Home'
import './index.styl'
  1. 为了简化事情,我创建了Array对象的原型 JavaScript 方法。这是一种可以用来改变某些函数工作方式的 JavaScript 方法的高级实现。特别地,我创建了一个异步的for循环,可以被await,以确保它在继续执行代码的其余部分之前完成,如下面的代码片段所示。基本上,这是一种干净的运行循环的方法:
Array.prototype.asyncForEach = function (callback) {
 return new Promise(resolve => {
 for(let i = 0; i < this.length; i++) {
 callback(this[i], i, this)
 }
 resolve()
 })
}
  1. 在您的构造函数中,包括一个setup()函数调用,如下面的代码片段所示:
constructor(props) {
    super(props)
    // State object omitted for simplicity
    this.setup()
}
  1. 使用以下代码片段展示的代码来实现setup()函数,并启动 web3 实例和显示产品:
async setup() {
    // Create the contract instance
    window.myWeb3 = new MyWeb3(ethereum)
    try {
        await ethereum.enable();
    } catch (error) {
        console.error('You must approve this dApp to interact with it')
    }
    const user = (await myWeb3.eth.getAccounts())[0]
    let products = []
    for(let i = 0; i < this.state.products.length; i++) {
        products[i] = this.state.products[i]
        products[i].owner = user
    }
    this.setState({products})
    this.displayProducts()
}
  1. 我们添加了对displayProducts()函数的调用,这将用于通过循环遍历我们state对象中的产品数组来显示产品,如下面的代码片段所示:
async displayProducts() {
    let productsHtml = []
    await this.state.products.asyncForEach(product => {
        productsHtml.push((
            <div key={product.id} className="product">
                <img className="product-image" src={product.image} />
                <div className="product-data">
                    <h3 className="product-title">{product.title}</h3>
                    <div className="product-description">{product.description.substring(0, 50) + '...'}</div>
                    <div className="product-price">{product.price} ETH</div>
                    <button onClick={() => {
                        this.setState({product})
                        this.redirectTo('/product')
                    }} className="product-view" type="button">View</button>
                </div>
            </div>
        ))
    })
    this.setState({productsHtml})
}
  1. 修改render()函数,并包括一个名为redirectTo()的函数,当用户使用 React 路由器点击按钮时,将允许您更改页面,如下面的代码片段所示:
    redirectTo(location) {
 this.props.history.push({
 pathname: location
 })
 }

    render() {
        return (
            <div>
                <Route path="/" exact render={() => (
 <Home
 productsHtml={this.state.productsHtml}
 />
                )} />
            </div>
        )
    }
}

我们对此索引文件进行了以下重要的添加:

  • 首先,我们为Array对象设置了一个自定义的原型函数,名为asyncForEach。你可能对 JavaScript 的深层原理不熟悉,但你必须了解所有类型的变量都是带有名为prototype的属性的对象,该属性包含该变量类型的方法。默认的forEach方法在 JavaScript 的某个地方被定义为Array.prototype.forEach = function() {...};这样做的目的是创建一个自定义的for循环,我们可以在这里使用await,以充分利用async函数。因此,我们可以用await array.asyncForEach()来代替for(let i = 0; i < array.length; i++) {},这样更容易阅读,且减少了混乱的代码。这只是我想要用来提高代码可读性和增加可用性的一种实现。

  • 然后我们导入了Home组件而不是Header组件,并在Routerender()函数内替换了它。

  • redirectTo函数通过使用我们之前看到的withRouter历史对象加载新页面来更改我们当前所见的Route。当用户点击displayProducts函数内的View按钮时,将使用此函数。

  • 在此之后,我们添加了一个setup函数,该函数配置 MetaMask 并将所有这些示例产品的所有者地址添加到其中,这样你就可以看到谁拥有这些物品。

  • 最后,我们创建了一个名为displayProducts()的函数,用于为每个产品生成 HTML,并将其推入产品数组并更新状态。Home组件然后将这些产品作为prop接收,并显示每个产品。

现在我们可以添加一些 CSS 代码来改善主页的外观,如下所示:

.products-container
    display: grid
    width: 80%
    margin: auto
    grid-template-columns: 1fr 1fr 1fr
    justify-items: center
    margin-top: 50px

    .product
        width: 400px
        border: 1px solid black

        .product-image
            width: 100%
            grid-column: 1 / 3
            box-shadow: 0 3px 0px 0 lightgrey

        .product-data
            display: grid
            grid-template-columns: 1fr 1fr
            grid-template-rows: 50px 20px 40px
            align-items: center
            padding: 10px productPadding
            grid-column-gap: productPadding
            background-color: white

            .product-description
                font-size: 10pt

            .product-price
                font-size: 11pt

            .product-view
                width: 200px
                grid-column: 2 / 3
                margin-top: 50px
                height: 50px

.spacer
    height: 200px
    width: 100%

现在网页的外观如下所示:

正如您所见,我们正在快速进展!对于这些类型的复杂应用程序,初始设置需要一些时间,但之后它是一件很棒的事情,因为您可以轻松更新每个单独的部分,同时为未来的改进保证了良好的可维护性因素。电子商务商店的主题与许多鞋店相似:它使用扁平设计和黑色调,同时也弹出一个元素,如按钮,以赋予它三维感。它让我想起了时尚杂志。

创建产品组件

现在我们有了一个基本的设计,当用户点击“查看”按钮时,我们可以创建产品页面,以便用户可以详细了解有关特定产品的更多信息。用户将能够在产品页面内购买产品。让我们按照以下步骤进行:

  1. 在您的组件内添加一个新的 Product.js 文件,并使用以下代码,尽管我总是建议您在查看解决方案之前自己尝试:
import React from 'react'
import Header from './Header'

class Product extends React.Component {
    constructor() { super() }
    render() {
        return (
            <div>
                <Header />
                <div className="product-details">
                    <img className="product-image" src={this.props.product.image} />
                    <div className="product-data">
                        <h3 className="product-title">{this.props.product.title}</h3>
                        <ul className="product-description">
                            {this.props.product.description.split('\n').map((line, index) => (
                                <li key={index}>{line}</li>
                            ))}
                        </ul>
                        <div className="product-data-container">
                            <div className="product-price">{this.props.product.price} ETH</div>
                            <div className="product-quantity">{this.props.product.quantity} units available</div>
                        </div>
                        <button onClick={() => {
                            this.props.redirectTo('/buy')
                        }} className="product-buy" type="button">Buy</button>
                    </div>
                </div>
            </div>
        )
    }
}

export default Product
  1. 我们需要一个新的页眉,因为当我们更改页面时,将加载一个新的组件(在本例中是 Product 组件),所以我们需要仅向 Product 组件显示必要的信息。然后,我们可以将其导入到索引文件中的新 Route 中,如下面的代码所示:
import React from 'react'
import ReactDOM from 'react-dom'
import MyWeb3 from 'web3'
import { BrowserRouter, Route, withRouter } from 'react-router-dom'
import Home from './components/Home'
import Product from './components/Product'
import './index.styl'

class Main extends React.Component {
    // Omitted previous code to keep the demonstration short 

    render() {
        return (
            <div>
                <Route path="/" exact render={() => (
                    <Home
                        productsHtml={this.state.productsHtml}
                    />
                )} />
 <Route path="/product" render={() => (
 <Product
 product={this.state.product}
 />
                )} />
            </div>
        )
    }
}
  1. 当您单击“查看”按钮时,应该能够访问自定义产品页面,假设我们已设置所需的历史功能。当用户单击“查看”按钮时,Product 组件的 product 属性也会设置。添加以下 CSS 代码以修复产品页面的设计:
.product-details
    display: grid
    width: 70%
    margin: auto
    grid-template-columns: 70% 30%
    grid-template-rows: 1fr
    margin-bottom: 50px
    grid-column-gap: 40px

    .product-image
        grid-column: 1 / 2
        justify-self: center

    .product-title, .product-description, .product-price, .product-buy
        grid-column: 2 / 3

    .product-description
        white-space: pre-wrap
        line-height: 20pt

    .product-data-container
        display: flex
        justify-content: space-between
        margin-bottom: 20px
  1. 您可以打开您的 dApp,点击产品的“查看”按钮,查看详细的产品页面,其中显示更大的图片和完整的描述,如下面的截图所示:

剩下的是添加购买、销售和订单页面。以下是我们如何使用 Buy 组件的方法,当用户单击位于产品页面中的“购买”按钮时,它将被显示:

  1. 导入所需的库,使用以下代码:
import React, { Component } from 'react'
import Header from './Header'
  1. Buy 组件内定义构造函数,其中状态变量为空,这样您就知道整个组件中将使用哪些变量,您可以使用以下代码完成此操作:
class Buy extends Component {
    constructor() {
        super()
        this.state = {
            nameSurname: '',
            lineOneDirection: '',
            lineTwoDirection: '',
            city: '',
            stateRegion: '',
            postalCode: '',
            country: '',
            phone: '',
        }
    }
  1. render 页面函数将显示一些基本的产品信息,以通知买家他们将获得什么,如下所示的代码:
    render() {
        return (
            <div>
                <Header />
                <div className="product-buy-page">
                    <h3 className="title">Product details</h3>
                    <img className="product-image" src={this.props.product.image} />
                    <div className="product-data">
                        <p className="product-title">{this.props.product.title}</p>
                        <div className="product-price">{this.props.product.price} ETH</div>
                    </div>
                </div>
  1. 包含一个区块,用于用户输入其地址以便可以免费获得产品的运输信息,如下面的代码所示:
                <div className="shipping-buy-page">
                    <h3>Shipping</h3>
                    <input onChange={e => {
                        this.setState({nameSurname: e.target.value})
                    }} placeholder="Name and surname..." type="text" />
                    <input onChange={e => {
                        this.setState({lineOneDirection: e.target.value})
                    }} placeholder="Line 1 direction..." type="text" />
                    <input onChange={e => {
                        this.setState({lineTwoDirection: e.target.value})
                    }} placeholder="Line 2 direction..." type="text" />
                    <input onChange={e => {
                        this.setState({city: e.target.value})
                    }} placeholder="City..." type="text" />
                    <input onChange={e => {
                        this.setState({stateRegion: e.target.value})
                    }} placeholder="State or region..." type="text" />
                    <input onChange={e => {
                        this.setState({postalCode: e.target.value})
                    }} placeholder="Postal code..." type="number" />
                    <input onChange={e => {
                        this.setState({country: e.target.value})
                    }} placeholder="Country..." type="text" />
                    <input onChange={e => {
                        this.setState({phone: e.target.value})
                    }} placeholder="Phone..." type="number" />
                    <button>Buy now to this address</button>
                </div>
            </div>
  1. 导出组件,以便可以将其导入到您的路由管理器中,如下面的代码所示:
export default Buy

我们只需要显示一个带有用户地址参数的表单,因为这是我们唯一需要的信息。我们可以假设运费都是免费的,已包含在价格中。我们将更新此Buy组件的状态以包含详细信息,以便稍后将这些数据提交给智能合约。然后在索引文件的开头导入Buy组件。我已经为您突出显示了新的导入位置,以便您看到Buy组件应该位于何处,如下面的代码所示:

import React from 'react'
import ReactDOM from 'react-dom'
import MyWeb3 from 'web3'
import { BrowserRouter, Route, withRouter } from 'react-router-dom'
import Home from './components/Home'
import Product from './components/Product'
import Buy from './components/Buy'
import './index.styl'

然后在您的render函数中添加新的Routeprops参数到刚刚导入的Buy组件中。更改已突出显示,以便您可以更快地找到它们,如下面的代码所示:

class Main extends React.Component {
    // Omitted the other functions to keep it short

    render() {
        return (
            <div>
                <Route path="/" exact render={() => (
                    <Home
                        productsHtml={this.state.productsHtml}
                    />
                )} />
                <Route path="/product" render={() => (
                    <Product
                        product={this.state.product}
                        redirectTo={location => this.redirectTo(location)}
                    />
                )} />
 <Route path="/buy" render={() => (
 <Buy
 product={this.state.product}
 />
 )} />
            </div>
        )
    }
}

我们只需要将state.product发送到此组件,以便我们可以看到正在购买的产品。添加一些 CSS 代码,通过执行以下步骤使其看起来好看:

  1. 使用以下代码为Buy组件的产品部分添加 CSS 代码:
.product-buy-page
    display: grid
    margin: auto
    width: 50%
    padding: 20px
    padding-top: 0
    grid-template-columns: 50% 50%
    grid-template-rows: auto 1fr
    margin-bottom: 50px
    grid-column-gap: 40px
    border: 1px solid black
    background-color: white

    .title
        grid-column: 1 / 3
        justify-self: center

    .product-image
        grid-column: 1 / 2
        height: 150px
        justify-self: end

    .product-title
        margin-bottom: 25px

    .product-price
        font-size: 15pt
        font-weight: bold
  1. 添加Buy组件的运输表单的 CSS 代码,如下面的代码所示:
.shipping-buy-page
    display: grid
    flex-direction: column
    justify-items: center
    width: 50%
    margin: auto
    margin-bottom: 200px

    input
        margin-bottom: 10px
        width: 100%

创建Sell组件

我们正在构建一个分散式市场,全世界的用户都可以通过发布自己的产品免费加入。不会收取任何费用,并且购买将以加密货币完成。因此,我们需要一个专门为这些卖家的页面,我们将通过以下步骤创建一个Sell组件:

  1. 导入必要的库以创建 React 组件,并包含Header:
import React from 'react'
import Header from './Header'
  1. 创建具有空构造函数的Sell类,其中包含用户将出售的产品的titledescriptionimagepricestate对象,如下面的代码所示:
class Sell extends React.Component {
    constructor() {
        super()
        this.state = {
            title: '',
            description: '',
            price: '',
            image: '',
        }
    }
}
  1. 创建具有整洁形式的render()函数,允许用户访问公共产品,如下面的代码所示。请注意,图像是一个字符串,因为我们将使用外部 URL 来提供图像,而不是自己托管文件:
render() {
    return (
        <div>
            <Header />
            <div className="sell-page">
                <h3>Sell product</h3>
                <input onChange={event => {
                    this.setState({title: event.target.value})
                }} type="text" placeholder="Product title..." />
                <textarea placeholder="Product description..." onChange={event => {
                    this.setState({description: event.target.value})
                }}></textarea>
                <input onChange={event => {
                    this.setState({price: event.target.value})
                }} type="text" placeholder="Product price in ETH..." />
                <input onChange={event => {
                    this.setState({image: event.target.value})
                }} type="text" placeholder="Product image URL..." />
                <p>Note that shipping costs are considered free so add the shipping price to the cost of the product itself</p>
                <button onClick={() => {
                    this.props.publishProduct(this.state)
                }} type="button">Publish product</button>
            </div>
        </div>
    )
}
  1. 使用以下代码导出此新组件,以便其他文件可以导入它:
export default Sell

在保存Sell组件之后,将其导入到您的索引 JavaScript 文件中。我们将添加一个名为publishProduct的函数,它将调用相应的智能合约函数。

下面的步骤显示了需要对索引文件进行的更改(以提高清晰度),以便导入此Sell组件:

  1. Buy组件导入下面直接导入Sell组件,如下面的代码所示:
import React from 'react'
import ReactDOM from 'react-dom'
import MyWeb3 from 'web3'
import { BrowserRouter, Route, withRouter } from 'react-router-dom'
import Home from './components/Home'
import Product from './components/Product'
import Buy from './components/Buy'
import Sell from './components/Sell'
import './index.styl'
  1. render()函数中包含Sell组件及其自己的route对象,同时定义一个publishProduct()函数,如下所示的函数:
class Main extends React.Component {
    // Omitted the other functions to keep it short

 async publishProduct(data) {}

    render() {
        return (
            <div>
                <Route path="/" exact render={() => (
                    <Home
                        productsHtml={this.state.productsHtml}
                    />
                )} />
                <Route path="/product" render={() => (
                    <Product
                        product={this.state.product}
                        redirectTo={location => this.redirectTo(location)}
                    />
                )} />
                <Route path="/buy" render={() => (
                    <Buy
                        product={this.state.product}
                    />
                )} /> <Route path="/sell" render={() => (
                    <Sell
                        publishProduct={data => this.publishProduct(data)}
                    />
                )} />
            </div>
        )
    }
}
  1. 添加一些 CSS 代码以改善此页面的设计,如下所示的函数:
.sell-page
    display: grid
    flex-direction: column
    justify-items: center
    width: 50%
    margin: auto
    margin-bottom: 200px

    input, textarea
        width: 100%
        margin-bottom: 10px

您可以通过单击页眉中的Sell按钮来查看其外观,该按钮将重定向到/sellURL,加载Sell组件。

创建订单组件

通过以下步骤添加最终的Orders.js组件。在看解决方案之前,试着自己做一下,以便你练习一下使用一些stylus CSS 完成设计的技能。你会发现这比预期的时间要长,但是一切都是值得的:

  1. 导入所需的库,如下代码所示:
import React, { Component } from 'react'
import Header from './Header'
  1. 定义构造函数并添加一些虚构的订单,以便你可以看到它的外观,如下代码所示:
class Orders extends Component {
    constructor() {
        super()

        // We'll separate the completed vs the pending based on the order state
        this.state = {
            sellOrders: [{
                id: 1,
                title: 'Classic trendy shoes',
                description: 'New unique shoes for sale',
                date: Date.now(),
                owner: '',
                price: 12,
                image: 'https://cdn.shopify.com/s/files/1/2494/8702/products/Bjakin-2018-Socks-Running-Shoes-for-Men-Lightweight-Sports-Sneakers-Colors-Man-Sock-Walking-Shoes-Big_17fa0d5b-d9d9-46a0-bdea-ac2dc17474ce_400x.jpg?v=1537755930',
                purchasedAt: Date.now(),
                state: 'completed',
            }],
            pendingSellOrdersHtml: [],
            pendingBuyOrdersHtml: [],
            completedSellOrdersHtml: [],
            completedBuyOrdersHtml: [],
        }

        this.displayOrders()
    }
  1. 我们需要一个函数来通过从智能合约获取数据来获取用户的订单,同时标记订单为已完成。我们暂时不会实现这些函数,因为我们首先必须创建智能合约,如下代码所示:
    async getUserOrders() {}

    async markAsCompleted(product) {}
  1. 添加这些空函数,然后创建一个名为displayOrders()的函数,它将使用状态数据来输出生成的 HTML。首先定义内部使用的数组,如下代码所示:
async displayOrders() {
    let pendingSellOrdersHtml = []
    let pendingBuyOrdersHtml = []
    let completedSellOrdersHtml = []
    let completedBuyOrdersHtml = []
}
  1. 阅读不同顺序的对象以循环遍历它们,并生成结果有效的 JSX。根据产品状态分类产品,如下代码所示:
await this.state.sellOrders.asyncForEach(product => {
    if(product.state == 'pending') {
        pendingSellOrdersHtml.push(
            <div key={product.id} className="product">
                <img className="product-image" src={product.image} />
                <div className="product-data">
                    <h3 className="small-product-title">{product.title}</h3>
                    <div className="product-state">State: {product.state}</div>
                    <div className="product-description">{product.description.substring(0, 15) + '...'}</div>
                    <div className="product-price">{product.price} ETH</div>
                    <button className="small-view-button" onClick={() => {
                        this.props.setState({product})
                        this.props.redirectTo('/product')
                    }} type="button">View</button>
                    <button className="small-completed-button" onClick={() => {
                        this.markAsCompleted(product)
                    }} type="button">Mark as completed</button>
                </div>
            </div>
        )
  1. 如果卖单的状态是已完成,将其推入completedSellOrders数组中,因为我们想要根据它们的状态分类订单,如下代码所示。创建一个新的 HTML 块,因为它会略有不同,因为我们想要使用一个按钮来标记产品为已完成:
} else {
        completedSellOrdersHtml.push(
            <div key={product.id} className="product">
                <img className="product-image" src={product.image} />
                <div className="product-data">
                    <h3 className="product-title">{product.title}</h3>
                    <div className="product-state">State: {product.state}</div>
                    <div className="product-description">{product.description.substring(0, 15) + '...'}</div>
                    <div className="product-price">{product.price} ETH</div>
                    <button onClick={() => {
                        this.props.setState({product})
                        this.props.redirectTo('/product')
                    }} className="product-view" type="button">View</button>
                </div>
            </div>
        )
    }
})
  1. 使用相同的过程来设计buyOrders数组的 HTML,以循环遍历数组,如下代码所示:
await this.state.buyOrders.asyncForEach(product => {
    let html = (
        <div key={product.id} className="product">
            <img className="product-image" src={product.image} />
            <div className="product-data">
                <h3 className="product-title">{product.title}</h3>
                <div className="product-state">State: {product.state}</div>
                <div className="product-description">{product.description.substring(0, 15) + '...'}</div>
                <div className="product-price">{product.price} ETH</div>
                <button onClick={() => {
                    this.props.setState({product})
                    this.props.redirectTo('/product')
                }} className="product-view" type="button">View</button>
            </div>
        </div>
    )

    if(product.state == 'pending') pendingBuyOrdersHtml.push(html)
    else completedBuyOrdersHtml.push(html)
})
  1. 使用生成的 HTML 对象更新组件的状态,如下代码所示:
this.setState({pendingSellOrdersHtml, pendingBuyOrdersHtml, completedSellOrdersHtml, completedBuyOrdersHtml})
  1. 创建render()函数来展示这些生成的订单,如下代码所示:
    render() {
        return (
            <div>
                <Header />
                <div className="orders-page">
                    <div>
                        <h3 className="order-title">PENDING ORDERS AS A SELLER</h3>
                        {this.state.pendingSellOrdersHtml}
                    </div>
                    <div>
                        <h3 className="order-title">PENDING ORDERS AS A BUYER</h3>
                        {this.state.pendingBuyOrdersHtml}
                    </div>
                    <div>
                        <h3 className="order-title">COMPLETED SELL ORDERS</h3>
                        {this.state.completedSellOrdersHtml}
                    </div>
                    <div>
                        <h3 className="order-title">COMPLETED BUY ORDERS</h3>
                        {this.state.completedBuyOrdersHtml}
                    </div>
                </div>
            </div>
        )
    }
}
  1. 导出Orders组件对象,如下代码所示:
export default Orders

这是一个很长的代码,因为我们在状态对象中添加了一些样本订单数据,以显示订单页面的真实视图。你可以看到我们为每个产品添加了一个state属性,它显示了订单是待定还是已完成。这将在智能合约中设置。displayOrders函数生成每种类型订单的 HTML 对象,因为我们想要分离已完成和待定以及买入和卖出的订单,以便你可以看到所有重要信息。当实现智能合约时,订单将来自getUserOrders函数。添加一些 CSS 使其看起来不错。你可以在官方 GitHub 上检查到我的设计,网址为github.com/merlox/ecommerce-dapp,在src/文件夹内。

最后,你将会得到一个很酷的订单页面,如下截图所示:

关于 React 中的用户界面就是这样了!只是为了确保,一旦所有组件创建完成,你应该在src/文件夹里有以下文件:

  • components/

    • Buy.js

    • Header.js

    • Sell.js

    • Product.js

    • Home.js

    • Orders.js

  • index.ejs

  • index.js

  • index.styl

理解 ERC-721 代币

这种新类型的代币用于在我们的智能合约中生成独特的产品。ERC-721 标准已被官方以太坊团队批准,这意味着您可以将其用于各种应用程序,而且知道它将与依赖于此标准的工具和智能合约兼容。就像 ERC-20 代币催生了去中心化代币交易所一样,我们可以预期会创建去中心化 ERC-721 交易所和数字以及实物产品市场。

解释 ERC-721 函数

要理解 ERC-721 代币的工作原理,最好查看定义 ERC-721 代币的函数,这样你就可以理解它们的内部工作方式。以下是描述这些函数的列表:

  • balanceOf(owner): 返回给定地址所有代币数量的计数,该地址用户拥有的代币。

  • ownerOf(tokenId): 返回拥有特定代币 ID 的地址。

  • safeTransferFrom(from, to, tokenId, data): 给定授权后,将代币从一个地址发送到另一个地址,就像这个短语对 ERC-20 代币所做的那样。它被称为安全,因为如果接收方是一个合约,它会检查合约是否能够接收 ERC-721 代币,这意味着接收合约已实现了onERC721Received函数,这样你就不会把代币丢失给不能管理这些类型代币的合约。data参数可以省略,它只是包含您可能想要发送到to接收方地址的额外字节信息。from地址必须是当前所有者,所以您可以将此函数用作普通的transfer函数或transferFrom函数(您可能熟悉使用 ERC-20 代币)来批准向另一个地址发送代币。

  • transferFrom(from, to, tokenId): 这与前一个函数相同,但它不确保接收地址能够管理这些类型的代币,如果它是一个智能合约的话。

  • approve(to, tokenId): 用于向另一个所有者批准特定代币,以便他们可以随意使用它。

  • setApprovalForAll(operator, approved): 这是为另一个地址,即operator地址,创建您所有代币的授权,以便其管理您的整个余额。您可以通过将approved参数设置为false来撤销对特定操作员的访问权限。

  • getApproved(tokenId): 返回具有此代币授权的地址。

  • isApprovedForAll(owner, operator): 如果operator可以访问所有所有者的代币,则返回true

注意他们从 ERC-20 规范中删除了我们熟悉的transfer函数,因为它通过允许使用transferFromsafeTransferFrom函数作为普通转账或已批准的转账来简化了流程,从而省去了标准transfer函数的需要。

_mint(owner, tokenId)_burn(tokenId) 内部函数用于生成和删除代币;然而,它们在标准的 ERC721.sol 智能合约中不可用,因为它们是内部函数,这意味着你需要创建一个新合约,继承该 ERC-721 合约并实现自定义的 mint(owner, tokenId)burn(tokenId) 函数(去掉下划线),根据需要进行任何修改,因为我们希望限制谁能创建或删除代币。

你能想象每个人都能随心所欲地生成代币吗?那将违背拥有有价值代币的目的,所以他们强迫你使用有限访问权限创建自己的铸造函数,可能还带有 onlyOwner 修饰符。在我们的案例中,我们将允许卖家为其产品铸造新类型的 ERC-721 代币。

我们去中心化电子商务商店中的每个产品将代表一个唯一的 ERC-721 代币;这就是为什么我们不想为每个产品添加多个数量,因为我们将不得不创建几个唯一的 ERC-721 实例。另外,NFT 意味着每个代币在其不同的属性上都是独一无二的。与 ERC-20 相比,其中每个代币都是相同的,ERC-721 标准旨在用于唯一物品,如家庭产品、手工制品、艺术品或独特的数字资产,如游戏中的皮肤。有趣的是,你可以根据需要组合这两种标准以创建独特的代币,同时还能够生成相同的多个实例。

ERC-721 智能合约

现在你已经了解了这些类型的 NFT 如何工作,让我们来看一下 ERC-721 合约接口。实现可在 GitHub 上找到 github.com/merlox/ecommerce-dapp/blob/master/contracts/ERC721.sol,因为完整的代码太长无法在此显示:

pragma solidity ⁰.5.0;

contract IERC721{
    event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
    event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);
    event ApprovalForAll(address indexed owner, address indexed operator, bool approved);
    function balanceOf(address owner) public view returns (uint256 balance);
    function ownerOf(uint256 tokenId) public view returns (address owner);
    function approve(address to, uint256 tokenId) public;
    function getApproved(uint256 tokenId) public view returns (address operator);
    function setApprovalForAll(address operator, bool _approved) public;
    function isApprovedForAll(address owner, address operator) public view returns (bool);
    function transferFrom(address from, address to, uint256 tokenId) public;
    function safeTransferFrom(address from, address to, uint256 tokenId) public;
    function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory data) public;
}

这个合约与 ERC-20 合约非常相似,因为它们背后的基本思想是相同的。该合约用于生成许多具有独特的代币,其挖矿功能必须单独实现,因为你想控制谁能够创建代币,谁能够销毁它们。

在你的 contracts/ 文件夹中创建名为 ERC721.sol 的文件,并添加以下代码,我们马上就会用到它。我们将创建一个合约,该合约继承 ERC-721 智能合约以实现 mint() 函数,因为默认的 ERC-721 实现不可访问。在那里创建一个名为 Ecommerce.sol 的新文件,并使用以下代码导入 ERC721.sol 合约:

pragma solidity ⁰.5.0;

import './ERC721.sol';

Solidity 版本并不重要,只要功能相同即可。创建一个自定义实现你自己的 ERC-721 智能合约,继承这个合约,如下所示:

pragma solidity ⁰.5.0;

import './ERC721.sol';

/// @notice The Ecommerce Token that implements the ERC721 token with mint function
/// @author Merunas Grincalaitis <[email protected]>
contract EcommerceToken is ERC721 {
 address public ecommerce;
 bool public isEcommerceSet = false;
    /// @notice To generate a new token for the specified address
    /// @param _to The receiver of this new token
    /// @param _tokenId The new token id, must be unique
 function mint(address _to, uint256 _tokenId) public {
 require(msg.sender == ecommerce, 'Only the ecommerce contract can mint new tokens');
 _mint(_to, _tokenId);
 }

    /// @notice To set the ecommerce smart contract address
 function setEcommerce(address _ecommerce) public {
 require(!isEcommerceSet, 'The ecommerce address can only be set once');
 require(_ecommerce != address(0), 'The ecommerce address cannot be empty');
 isEcommerceSet = true;
 ecommerce = _ecommerce;
 }
}

此代币合约将仅允许电子商务合约生成新代币,在购买完成后将其转移到买家名下;在你能够铸造代币之前,必须设置 setEcommerce 函数。

开发电子商务智能合约

开发与 ERC-721 代币交互的智能合约很简单,因为我们只需确保用户的产品关联有一个代币 ID。如果用户希望这样做,他们将能够独立与他们的代币进行交互。对于我们的市场,我们将专注于创建购买和销售功能,以创建和销毁代币。像往常一样,我们还将创建多个 getter 从智能合约中提取数据供用户界面使用。

让我们开始创建电子商务合同,将所有市场逻辑放在同一个文件中,因为它不会占用太多空间:

  1. 定义智能合约所需的变量,从您需要的结构开始,如下面的代码所示:
/// @notice The main ecommerce contract to buy and sell ERC-721 tokens representing physical or digital products because we are dealing with non-fungible tokens, there will be only 1 stock per product
/// @author Merunas Grincalaitis <[email protected]>
contract Ecommerce {
    struct Product {
        uint256 id;
        string title;
        string description;
        uint256 date;
        address payable owner;
        uint256 price;
        string image;
    }
    struct Order {
        uint256 id;
        address buyer;
        string nameSurname;
        string lineOneDirection;
        string lineTwoDirection;
        bytes32 city;
        bytes32 stateRegion;
        uint256 postalCode;
        bytes32 country;
        uint256 phone;
        string state; // Either 'pending', 'completed'
    }
  1. 添加映射、数组、变量和构造函数,如下面的代码所示:
    // Seller address => products
    mapping(address => Order[]) public pendingSellerOrders; // The products waiting to be fulfilled by the seller, used by sellers to check which orders have to be filled
    // Buyer address => products
    mapping(address => Order[]) public pendingBuyerOrders; // The products that the buyer purchased waiting to be sent
    mapping(address => Order[]) public completedOrders;
    // Product id => product
    mapping(uint256 => Product) public productById;
    // Product id => order
    mapping(uint256 => Order) public orderById;
    Product[] public products;
    uint256 public lastId;
    address public token;

    /// @notice To setup the address of the ERC-721 token to use for this contract
    /// @param _token The token address
    constructor(address _token) public {
        token = _token;
    }
}

我们必须首先设置变量,从结构开始,这种情况下是ProductOrder。每个订单都将通过 ID 引用特定的产品,在这两种情况下 ID 将是相同的,这意味着每个产品将与具有相同 ID 的订单对应。将有映射用于尚未完成的待处理订单,并有其他映射用于已完成的订单,以便我们有已完成订单的参考。构造函数将接收令牌地址,以便电子商务合约可以创建新的代币。

创建发布功能

创建一个功能来发布新产品,以便用户可以通过以下代码自行出售产品。图像 URL 将是图像所在的位置:

/// @notice To publish a product as a seller
/// @param _title The title of the product
/// @param _description The description of the product
/// @param _price The price of the product in ETH
/// @param _image The image URL of the product
function publishProduct(string memory _title, string memory _description, uint256 _price, string memory _image) public {
    require(bytes(_title).length > 0, 'The title cannot be empty');
    require(bytes(_description).length > 0, 'The description cannot be empty');
    require(_price > 0, 'The price cannot be empty');
    require(bytes(_image).length > 0, 'The image cannot be empty');

    Product memory p = Product(lastId, _title, _description, now, msg.sender, _price, _image);
    products.push(p);
    productById[lastId] = p;
    EcommerceToken(token).mint(address(this), lastId); // Create a new token for this product which will be owned by this contract until sold
    lastId++;
}

此功能将检查参数,以便在设置参数的同时铸造新的代币。

创建购买功能

现在用户可以发布要出售的产品后,您可以开始编写buy功能来购买产品:

/// @notice To buy a new product, note that the seller must authorize this contract to manage the token
/// @param _id The id of the product to buy
/// @param _nameSurname The name and surname of the buyer
/// @param _lineOneDirection The first line for the user address
/// @param _lineTwoDirection The second, optional user address line
/// @param _city Buyer's city
/// @param _stateRegion The state or region where the buyer lives
/// @param _postalCode The postal code of his location
/// @param _country Buyer's country
/// @param _phone The optional phone number for the shipping company
function buyProduct(uint256 _id, string memory _nameSurname, string memory _lineOneDirection, string memory _lineTwoDirection, bytes32 _city, bytes32 _stateRegion, uint256 _postalCode, bytes32 _country, uint256 _phone) public payable {
    // The line 2 address and phone are optional, the rest are mandatory
    require(bytes(_nameSurname).length > 0, 'The name and surname must be set');
    require(bytes(_lineOneDirection).length > 0, 'The line one direction must be set');
    require(_city.length > 0, 'The city must be set');
    require(_stateRegion.length > 0, 'The state or region must be set');
    require(_postalCode > 0, 'The postal code must be set');
    require(_country > 0, 'The country must be set');

    Product memory p = productById[_id];
    require(bytes(p.title).length > 0, 'The product must exist to be purchased');
    Order memory newOrder = Order(_id, msg.sender, _nameSurname, _lineOneDirection, _lineTwoDirection, _city, _stateRegion, _postalCode, _country, _phone, 'pending');
    require(msg.value >= p.price, "The payment must be larger or equal than the products price");

    // Delete the product from the array of products
    for(uint256 i = 0; i < products.length; i++) {
        if(products[i].id == _id) {
            Product memory lastElement = products[products.length - 1];
            products[i] = lastElement;
            products.length--;
        }
    }

    // Return the excess ETH sent by the buyer
    if(msg.value > p.price) msg.sender.transfer(msg.value - p.price);
    pendingSellerOrders[p.owner].push(newOrder);
    pendingBuyerOrders[msg.sender].push(newOrder);
    orderById[_id] = newOrder;
    EcommerceToken(token).transferFrom(address(this), msg.sender, _id); // Transfer the product token to the new owner
    p.owner.transfer(p.price);
}

首先,buy功能必须是可支付的,以便用户可以用以太币发送所需的价格,这些价格将被发送给卖方,除了燃气成本之外没有任何费用。购买产品时,买方需要发送所有地址详细信息,以便卖方可以处理发货;这就是为什么buy功能中有这么多参数的原因,其中电话号码和第二地址行是可选的。products数组会删除产品,以便用户界面显示最新的产品。将创建一个新的order结构实例,并将订单添加到待处理映射中。

创建标记订单功能

创建订单后,我们需要一种方法告诉客户产品已经发货。我们可以通过一个名为markOrderCompleted的新功能来做到这一点,如下面的代码所示:

/// @notice To mark an order as completed
/// @param _id The id of the order which is the same for the product id
function markOrderCompleted(uint256 _id) public {
    Order memory order = orderById[_id];
    Product memory product = productById[_id];
    require(product.owner == msg.sender, 'Only the seller can mark the order as completed');
    order.state = 'completed';

    // Delete the seller order from the array of pending orders
    for(uint256 i = 0; i < pendingSellerOrders[product.owner].length; i++) {
        if(pendingSellerOrders[product.owner][i].id == _id) {
            Order memory lastElement = orderById[pendingSellerOrders[product.owner].length - 1];
            pendingSellerOrders[product.owner][i] = lastElement;
            pendingSellerOrders[product.owner].length--;
        }
    }
    // Delete the seller order from the array of pending orders
    for(uint256 i = 0; i < pendingBuyerOrders[order.buyer].length; i++) {
        if(pendingBuyerOrders[order.buyer][i].id == order.id) {
            Order memory lastElement = orderById[pendingBuyerOrders[order.buyer].length - 1];
            pendingBuyerOrders[order.buyer][i] = lastElement;
            pendingBuyerOrders[order.buyer].length--;
        }
    }
    completedOrders[order.buyer].push(order);
    orderById[_id] = order;
}

这个函数从各自的数组中移除了待处理订单,并将它们移到 completedOrders 映射中。我们不使用 delete 函数,而是减少数组的长度来删除 Order,因为 delete 函数实际上并不从数组中删除用户订单,而是留下一个空的订单实例。当我们将要 delete 的元素移动到数组的最后位置并减少其长度时,我们完全删除了它,而不会留下任何空洞,因为 delete 函数保持数组完整。

创建 getter 函数

剩下的就是添加所需的 getter 函数来返回这些数组的长度,因为公共数组变量不会公开数组长度,我们需要知道有多少产品和订单以向用户显示最新内容,让我们使用以下代码来设置:

/// @notice Returns the product length
/// @return uint256 The number of products
function getProductsLength() public view returns(uint256) {
    return products.length;
}

/// @notice To get the pending seller or buyer orders
/// @param _type If you want to get the pending seller, buyer or completed orders
/// @param _owner The owner of those orders
/// @return uint256 The number of orders to get
function getOrdersLength(bytes32 _type, address _owner) public view returns(uint256) {
    if(_type == 'seller') return pendingSellerOrders[_owner].length;
    else if(_type == 'buyer') return pendingBuyerOrders[_owner].length;
    else if(_type == 'completed') return completedOrders[_owner].length;
}

getOrdersLength() 函数将被用于卖家、买家或已完成的三种订单类型,以避免创建多个相似的函数。这就是整个合同。如果你想查看更新版本,请访问我的 GitHub:github.com/merlox/ecommerce-dapp

部署智能合约

了解部署过程是很重要的,以确保成功执行,因为,让我们面对现实吧,Truffle 可能会让人感到困惑。在之前的章节中,你已经看到了使用这个框架部署智能合约需要做什么,但再过一遍这个过程也无妨,只是为了确保你理解了它。

首先,打开你的 truffle-config.js 文件,并修改它以使用 ropsten,这是我们将用于部署我们 dApp 的初始版本的网络。以下是它的样子,使用你自己的 INFURA 密钥:

const HDWalletProvider = require('truffle-hdwallet-provider');
const infuraKey = "v3/<YOUR-INFURA-KEY-HERE>;
const fs = require('fs');
const mnemonic = fs.readFileSync(".secret").toString().trim();

module.exports = {
  networks: {
    development: {
     host: "127.0.0.1", // Localhost (default: none)
     port: 8545, // Standard Ethereum port (default: none)
     network_id: "*", // Any network (default: none)
    },
    ropsten: {
      provider: () => new HDWalletProvider(mnemonic, `https://ropsten.infura.io/${infuraKey}`),
      network_id: 3, // Ropsten's id
      gas: 5500000, // Ropsten has a lower block limit than mainnet
      confirmations: 2, // # of confs to wait between deployments. (default: 0)
      timeoutBlocks: 200, // # of blocks before a deployment times out (minimum/default: 50)
      skipDryRun: true // Skip dry run before migrations? (default: false for public nets )
    }
  }
}

我保留了开发网络的可用性,因为在将合同部署到 ropsten 之前,你可能需要在使用 ganache-cli 生成的本地测试网络上检查部署过程。在这样做时,请确保你的 .secret 文件中的种子短语的第一个帐户中有足够的 ropsten 以太币。记得安装 Truffle 钱包,以使以下代码的部署过程正常工作:

npm i -S truffle-hdwallet-provider

然后,在你的 migrations/ 文件夹中创建一个 .secret 文件,并创建一个名为 2_deploy_contracts.js 的文件,告诉 Truffle 在部署合同时需要做什么,主要是设置构造函数参数,如下面的代码所示。如果没有这个文件,Truffle 将无法部署:

const Token = artifacts.require("./EcommerceToken.sol")
const Ecommerce = artifacts.require("./Ecommerce.sol")
let token

module.exports = function(deployer, network, accounts) {
    deployer.deploy(
        Token,
        { gas: 8e6 }
    ).then(tokenInstance => {
        token = tokenInstance
        return deployer.deploy(Ecommerce, token.address, {
            gas: 8e6
        })
    }).then(async ecommerce => {
        await token.contract.methods.setEcommerce(ecommerce.address).send({
            from: accounts[0]
        })
        console.log('Is set?', await token.contract.methods.isEcommerceSet().call())
        console.log('Deployed both!')
    })
}

您的迁移文件夹应该有1_initial_migrations.js2_deploy_contracts.js文件。语法有点混乱,但重要的是我们使用了deployer.deploy()函数,该函数返回一个 promise 来获取令牌地址,并从令牌合约运行setEcommerce()函数,以便我们可以立即开始使用合约。请注意,我们通过将第三个参数添加到主函数来访问accounts,这是运行setEcommerce()函数所必需的,第一个以太坊地址。最后,我通过调用令牌中的isEcommerceSet()公共变量来检查电子商务合约是否已正确设置。

运行以下部署命令:

truffle deploy --network ropsten --reset

如果您想要测试一切是否正常运行而无需等待ropsten,您可以通过运行以下命令快速在ganache-cli私有区块链上部署它:

truffle deploy --network development --reset

部署您的合约后,您会在build/contract/Ecommerce.json文件夹中找到地址和 ABI。

完成 dApp

要完成 dApp,我们必须修改 React 代码以集成智能合约更改,同时理解我们如何使用正确的方法从区块链接收信息并正确显示该数据的方式。在此之前,请确保您的合约已部署到ropsten,如前几个步骤所示。

设置合约实例

因为我们使用 webpack,所以我们可以从 React 文件中访问源文件夹中的所有文件,这意味着我们可以获取已部署的智能合约 ABI 和已部署的合约地址,以及创建合约实例所需的参数。这在以下代码中显示:

import React from 'react'
import ReactDOM from 'react-dom'
import MyWeb3 from 'web3'
import { BrowserRouter, Route, withRouter } from 'react-router-dom'
import Home from './components/Home'
import Product from './components/Product'
import Sell from './components/Sell'
import Header from './components/Header'
import Buy from './components/Buy'
import Orders from './components/Orders'

import './index.styl'
import ABI from '../build/contracts/Ecommerce.json'

当您使用 Truffle 成功部署您的智能合约时,将创建build文件夹,其中包含我们可能需要的重要智能合约参数。修改您的设置函数以全局访问合约对象,使外部组件更容易。我已经在下面的代码中突出显示了合约实例,供您查找更改:

async setup() {
    // Create the contract instance
    window.myWeb3 = new MyWeb3(ethereum)
    try {
        await ethereum.enable();
    } catch (error) {
        console.error('You must approve this dApp to interact with it')
    }
 window.user = (await myWeb3.eth.getAccounts())[0]
 window.contract = new myWeb3.eth.Contract(ABI.abi, ABI.networks['3'].address, {
 from: user
 })
    await this.getLatestProducts(9)
    await this.displayProducts()
}

注意我们如何将state对象减少为几个元素,而没有任何虚拟数据,因为我们将使用真实的智能合约数据。合约实例是通过使用abi和合约地址创建的,这些信息也包含在构建的 JSON 文件中。在设置函数的末尾,我们调用了getLatestProducts()displayProducts()函数,正如您即将看到的,这些函数是必要的,以便从合约中获取数据并正确显示它。

更新索引文件

现在我们有一个可工作的合约实例,我们可以在索引文件中工作,以便将功能保持在较小的组件中,如下面的代码所示:

  1. 实现displayProducts()函数以按属性排序显示产品:
async displayProducts() {
    let productsHtml = []
    if(this.state.products.length == 0) {
        productsHtml = (
            <div key="0" className="center">There are no products yet...</div>
        )
    }
    await this.state.products.asyncForEach(product => {
        productsHtml.push((
            <div key={product.id} className="product">
                <img className="product-image" src={product.image} />
                <div className="product-data">
                    <h3 className="product-title">{product.title}</h3>
                    <div className="product-description">{product.description.substring(0, 50) + '...'}</div>
                    <div className="product-price">{product.price} ETH</div>
                    <button onClick={() => {
                        this.setState({product})
                        this.redirectTo('/product')
                    }} className="product-view" type="button">View</button>
                </div>
            </div>
        ))
    })
    this.setState({productsHtml})
}
  1. 添加更新后的重定向功能,如下所示的代码所示:
redirectTo(location) {
  this.props.history.push({
    pathname: location
  })
}
  1. 实现从智能合约获取产品的功能,方法是获取这些产品的长度,并循环每一个:
async getLatestProducts(amount) {
    // Get the product ids
    const productsLength = parseInt(await contract.methods.getProductsLength().call())
    let products = []
    let condition = (amount > productsLength) ? 0 : productsLength - amount

    // Loop through all of them one by one
    for(let i = productsLength; i > condition; i--) {
        let product = await contract.methods.products(i - 1).call()
        product = {
            id: parseInt(product.id),
            title: product.title,
            date: parseInt(product.date),
            description: product.description,
            image: product.image,
            owner: product.owner,
            price: myWeb3.utils.fromWei(String(product.price)),
        }
        products.push(product)
    }
    this.setState({products})
}

在我们的主页上,我们将展示其他卖家添加的最新产品,以便您可以立即开始购买。因此,我们将使用getLatestProducts(),它接收要显示的产品数量作为参数,同时从区块链获取数据。那么,我们如何在没有getter函数的情况下获取所有产品数据呢?好吧,流程是这样的:

  1. 我们获取产品数组的长度。我们使用getProductsLength()函数,因为如果没有适当的getter函数,我们无法获取数组的长度。

  2. 一旦我们知道智能合约中有多少产品可用,我们就通过循环该大小来运行products()函数,该函数可用于我们的产品数组,这意味着它自动为其创建了getter函数。公共数组必须逐个访问;这就是为什么我们使用反向for循环的原因。

  3. 我们需要一个反向循环来首先获取最新的产品。关于for循环的工作原理,因为可能出现我们要显示9时产品已用尽的情况,这是由于当我们想要显示9时,我们从零产品开始。这就是为什么我们创建了condition变量-它检查要求显示的产品数量是否实际可用;如果不可用,我们只需获取所有可用的产品,无论它们有多少。

另一方面,一旦state对象被填充了包含在我们的智能合约中的产品,我们就使用displayProducts()函数,该函数负责生成每个产品所需的正确 HTML,同时更新productsHtml状态数组。

最后,我们有render函数,这些新更新的组件略有修改,如下所示的代码所示:

render() {
    return (
        <div>
            <Route path="/product" render={() => (
                <Product
                    product={this.state.product}
                    redirectTo={location => this.redirectTo(location)}
                />
            )}/>
            <Route path="/sell" render={() => (
                <Sell
                    publishProduct={data => this.publishProduct(data)}
                />
            )}/>
            <Route path="/buy" render={() => (
                <Buy
                    product={this.state.product}
                />
            )} />
            <Route path="/orders" render={() => (
                <Orders
                    setState={state => this.setState(state)}
                    redirectTo={location => this.redirectTo(location)}
                />
            )} />
            <Route path="/" exact render={() => (
                <Home
                    productsHtml={this.state.productsHtml}
                />
            )} />
        </div>
    )
}

在进行实现更改后,请查看整个索引文件,可在 GitHub 上找到,网址为 github.com/merlox/ecommerce-dapp

更新购买组件

让我们转向Buy.js文件,因为Home.jsProduct.js组件将保持原样,无需任何必要的修改,考虑到产品数据将具有相同的预期格式。在Buy组件中,我们需要添加一个购买产品的函数,该函数将事务发送到智能合约,以下是该函数:

async buyProduct() {
    await contract.methods.buyProduct(this.props.product.id, this.state.nameSurname, this.state.lineOneDirection, this.state.lineTwoDirection, this.bytes32(this.state.city), this.bytes32(this.state.stateRegion), this.state.postalCode, this.bytes32(this.state.country), this.state.phone).send({
        value: myWeb3.utils.toWei(this.props.product.price)
    })
}

bytes32(name) {
    return myWeb3.utils.fromAscii(name)
}

buyProduct()函数获取与用户地址相关的所有状态数据,并将具有所需产品价格的交易作为交易的支付发送。bytes32函数是必需的,以将一些字符串值转换为bytes32,以节省 gas 成本。这就是此特定组件所需的所有更改。在更新的 GitHub 上检查整个组件的最终实现:github.com/merlox/ecommerce-dapp/blob/master/src/components/Buy.js

更新出售组件

让我们来创建Sell.js功能所需的功能,这样你就可以开始向市场添加可购买的产品了。在这种情况下,我们需要添加一个函数,该函数将从智能合约中调用publishProduct()函数。下面是更新后的publish函数的样子:

async publishProduct() {
    if(this.state.title.length == 0) return alert('You must set the title before publishing the product')
    if(this.state.description.length == 0) return alert('You must set the description before publishing the product')
    if(this.state.price.length == 0) return alert('You must set the price before publishing the product')
    if(this.state.image.length == 0) return alert('You must set the image URL before publishing the product')

    await contract.methods.publishProduct(this.state.title, this.state.description, myWeb3.utils.toWei(this.state.price), this.state.image).send()
}

注意我们如何检查所有必需的参数,以便让用户知道何时缺少某些内容。你可以添加一些额外的检查,以确保提供的图片 URL 实际上是一个可以在市场上显示的有效图片。这部分就交给你了。不应该花费你超过10 分钟的时间,这是一个练习你的 JavaScript 技能的好机会。

最终更新的版本在 GitHub 上可用:github.com/merlox/ecommerce-dapp/blob/master/src/components/Sell.js

更新订单组件

现在让我们更新Orders.js组件,这是最复杂的组件,因为我们必须生成多个产品。让我们从创建一个函数开始,以获取与当前用户相关的所有订单,如下所示:

async getOrders(amount) {
    const pendingSellerOrdersLength = parseInt(await contract.methods.getOrdersLength(this.bytes32('seller'), user).call())
    const pendingBuyerOrdersLength = parseInt(await contract.methods.getOrdersLength(this.bytes32('buyer'), user).call())
    const completedOrdersLength = parseInt(await contract.methods.getOrdersLength(this.bytes32('completed'), user).call())

    const conditionSeller = (amount > pendingSellerOrdersLength) ? 0 : pendingSellerOrdersLength - amount
    const conditionBuyer = (amount > pendingBuyerOrdersLength) ? 0 : pendingBuyerOrdersLength - amount
    const conditionCompleted = (amount > completedOrdersLength) ? 0 : completedOrdersLength - amount

    let pendingSellerOrders = []
    let pendingBuyerOrders = []
    let completedOrders = []

    // In reverse to get the most recent orders first
    for(let i = pendingSellerOrdersLength; i > conditionSeller; i--) {
        let order = await contract.methods.pendingSellerOrders(user, i - 1).call()
        pendingSellerOrders.push(await this.generateOrderObject(order))
    }

    for(let i = pendingBuyerOrdersLength; i > conditionBuyer; i--) {
        let order = await contract.methods.pendingBuyerOrders(user, i - 1).call()
        pendingBuyerOrders.push(await this.generateOrderObject(order))
    }

    for(let i = completedOrdersLength; i > conditionCompleted; i--) {
        let order = await contract.methods.completedOrders(user, i - 1).call()
        completedOrders.push(await this.generateOrderObject(order))
    }

    this.setState({pendingSellerOrders, pendingBuyerOrders, completedOrders})
}

我们通过遵循与索引文件中产品相同的程序生成了三个不同的数组。我们具有相同的条件运算符,但用于不同类型的订单。然后,我们为每个所需订单运行一个逆序的for循环,以便获得最近的订单。由于智能合约返回的数据有些混乱,我们创建了一个名为generateOrderObject()的函数,该函数接收一个订单对象,并返回一个已清理的对象,其中包含已转换为可读文本的十六进制值。下面是它的样子:

async generateOrderObject(order) {
    let productAssociated = await contract.methods.productById(parseInt(order.id)).call()
    order = {
        id: parseInt(order.id),
        buyer: order.buyer,
        nameSurname: order.nameSurname,
        lineOneDirection: order.lineOneDirection,
        lineTwoDirection: order.lineTwoDirection,
        city: myWeb3.utils.toUtf8(order.city),
        stateRegion: myWeb3.utils.toUtf8(order.stateRegion),
        postalCode: String(order.postalCode),
        country: myWeb3.utils.toUtf8(order.country),
        phone: String(order.phone),
        state: order.state,
        date: String(productAssociated.date),
        description: productAssociated.description,
        image: productAssociated.image,
        owner: productAssociated.owner,
        price: myWeb3.utils.fromWei(String(productAssociated.price)),
        title: productAssociated.title,
    }
    return order
}

将重复的代码分离到外部函数中以保持代码整洁是很重要的。正如你所看到的,这个函数将变量的字节类型转换为可读的utf8字符串,同时将大数转换为整数,以便它们可以在我们的用户界面中正确显示。

在使用最新订单更新状态对象之后,我们可以创建一个函数,通过以下步骤生成每个元素的正确 HTML:

  1. 设置所需的数组变量,这种情况下更简单,因为我们要为不同类型的订单创建三个块:
async displayOrders() {
    let pendingSellerOrdersHtml = []
    let pendingBuyerOrdersHtml = []
    let completedOrdersHtml = []
  1. 如果没有每种类型订单,我们希望显示一条消息,让用户知道没有订单,使用以下代码:
    if(this.state.pendingSellerOrders.length == 0) {
        pendingSellerOrdersHtml.push((
            <div key="0" className="center">There are no seller orders yet...</div>
        ))
    }
    if(this.state.pendingBuyerOrders.length == 0) {
        pendingBuyerOrdersHtml.push((
            <div key="0" className="center">There are no buyer orders yet...</div>
        ))
    }
    if(this.state.completedOrders.length == 0) {
        completedOrdersHtml.push((
            <div key="0" className="center">There are no completed orders yet...</div>
        ))
    }
  1. 使用以下代码添加地址部分来更新待处理订单:
    await this.state.pendingSellerOrders.asyncForEach(order => {
        pendingSellerOrdersHtml.push(
            <div key={order.id} className="product">
                <img className="product-image" src={order.image} />
                <div className="product-data">
                    <h3 className="small-product-title">{order.title}</h3>
                    <div className="product-state">State: {order.state}</div>
                    <div className="product-description">{order.description.substring(0, 15) + '...'}</div>
                    <div className="product-price">{order.price} ETH</div>
                    <button className="small-view-button" onClick={() => {
                        this.props.setState({product: order})
                        this.props.redirectTo('/product')
                    }} type="button">View</button>
                    <button className="small-completed-button" onClick={() => {
                        this.markAsCompleted(order.id)
                    }} type="button">Mark as completed</button>
                </div>
  1. 在产品数据下面,使用以下代码添加地址信息,以便卖家可以履行这些订单:
                <div className="order-address">
                    <div>Id</div>
                    <div className="second-column" title={order.id}>{order.id}</div>
                    <div>Buyer</div>
                    <div className="second-column" title={order.buyer}>{order.buyer}</div>
                    <div>Name and surname</div>
                    <div className="second-column" title={order.nameSurname}>{order.nameSurname}</div>
                    <div>Line 1 direction</div>
                    <div className="second-column" title={order.lineOneDirection}>{order.lineOneDirection}</div>
                    <div>Line 2 direction</div>
                    <div className="second-column" title={order.lineTwoDirection}>{order.lineTwoDirection}</div>
                    <div>City</div>
                    <div className="second-column" title={order.city}>{order.city}</div>
                    <div>State or region</div>
                    <div className="second-column" title={order.stateRegion}>{order.stateRegion}</div>
                    <div>Postal code</div>
                    <div className="second-column">{order.postalCode}</div>
                    <div>Country</div>
                    <div className="second-column" title={order.country}>{order.country}</div>
                    <div>Phone</div>
                    <div className="second-column">{order.phone}</div>
                    <div>State</div>
                    <div className="second-column" title={order.state}>{order.state}</div>
                </div>
            </div>
        )
    })
  1. 对于待处理买家订单,我们采取相同的做法:我们首先显示产品数据,使用以下代码:
 await this.state.pendingBuyerOrders.asyncForEach(order => {
        pendingBuyerOrdersHtml.push(
            <div key={order.id} className="product">
                <img className="product-image" src={order.image} />
                <div className="product-data">
                    <h3 className="product-title">{order.title}</h3>
                    <div className="product-state">State: {order.state}</div>
                    <div className="product-description">{order.description.substring(0, 15) + '...'}</div>
                    <div className="product-price">{order.price} ETH</div>
                    <button onClick={() => {
                        this.props.setState({product: order})
                        this.props.redirectTo('/product')
                    }} className="product-view" type="button">View</button>
                </div>
  1. 地址数据将完全相同,因此将其复制并粘贴到待处理买家订单循环中。我们使用相同的代码,因为我们需要更新每个 HTML 块的外观,但类名必须不同。使用以下代码将for循环添加到已完成订单数组中:
    await this.state.completedOrders.asyncForEach(order => {
        completedOrdersHtml.push(
            <div key={order.id} className="product">
                <img className="product-image" src={order.image} />
                <div className="product-data">
                    <h3 className="product-title">{order.title}</h3>
                    <div className="product-state">State: {order.state}</div>
                    <div className="product-description">{order.description.substring(0, 15) + '...'}</div>
                    <div className="product-price">{order.price} ETH</div>
                    <button onClick={() => {
                        this.props.setState({product: order})
                        this.props.redirectTo('/product')
                    }} className="product-view" type="button">View</button>
                </div>
  1. 将地址块粘贴到产品数据下方。使用setState()方法更新此组件的状态:
    this.setState({pendingSellerOrdersHtml, pendingBuyerOrdersHtml, completedOrdersHtml})

这是一个大函数,因为我们为了保持简单而有重复的功能。我们有三个循环用于三个订单数组,这样我们可以将订单信息提供给用户。没有太多花哨的东西,只是干净的设计中的数据。我们将该数据添加到state对象中,以便我们可以轻松显示它。

  1. 创建一个setup()函数,在组件加载时运行这两个函数,如下所示:
bytes32(name) {
    return myWeb3.utils.fromAscii(name)
}

async setup() {
    await this.getOrders(5)
    await this.displayOrders()
}
  1. 在这种情况下,我们每种类型请求五个订单,因为我们不想让用户被信息压倒,这很容易根据您的喜好进行更改。您甚至可以在 UI 中添加一个滑块,以便用户更改显示的项目数量。render()函数也已更新以反映买家的地址数据,如下所示:
render() {
    return (
        <div>
            <Header />
            <div className="orders-page">
                <div>
                    <h3 className="order-title">PENDING ORDERS AS A SELLER</h3>
                    {this.state.pendingSellerOrdersHtml}
                </div>

                <div>
                    <h3 className="order-title">PENDING ORDERS AS A BUYER</h3>
                    {this.state.pendingBuyerOrdersHtml}
                </div>

                <div className="completed-orders-container">
                    <h3 className="order-title">COMPLETED ORDERS</h3>
                    {this.state.completedOrdersHtml}
                </div>
            </div>
        </div>
    )
}

这就是Orders组件的全部更改。请查看官方 GitHub 链接中的更新实现:github.com/merlox/ecommerce-dapp/blob/master/src/components/Orders.js

您可以在github.com/merlox/ecommerce-dapp/blob/master/src/index.styl找到更新后的 CSS 代码,您将获得完全相同的设计。

这就是整个电子商务 dApp!这是它的外观,只是为了让您看到这个简单而又功能强大的应用程序的潜力:

记得将你的智能合约部署到ropsten并运行npm run dev来启动 webpack 服务器,以便您可以与其交互。这是以太坊电子商务部门的一个原型;现在您理解了智能合约如何与用户界面交互,您可以在此基础上构建自己的想法。

请务必查看本章节代码的 GitHub 链接:github.com/merlox/ecommerce-dapp

摘要

在本章中,你首先学习了使用 ERC-721 代币利用去中心化智能合约技术创建独特产品市场的潜力,以便你可以轻松管理用户自由创建的 NFT。然后,你建立了一个清晰的界面来显示最重要的数据,使用户有一个舒适的地方与底层智能合约进行交互。接下来,你通过学习 NFT 代币的工作原理(包括所有功能)来构建了智能合约。你部署了你自己的 ERC-721 标准版本,然后创建了包含发布产品到公共市场所需逻辑的电子商务智能合约,以便其他人可以用真正的以太币购买它们。最后,你通过创建与 React 用户界面交互所需的必要功能将所有内容整合在一起。

在下一章中,我们将进一步构建一个去中心化银行和借贷平台,实现复杂的智能合约系统,以确保人们可以访问安全资金储备,并为他们提供用户界面进行交互。

标签:精通,函数,以太,创建,区块,合约,我们
From: https://www.cnblogs.com/apachecn/p/18169385

相关文章

  • SQL SERVER 从入门到精通 第5版 第三篇 高级应用 第12章 游标的使用 读书笔记
     第十二章游标的使用>.游标的概述游标是一种数据库对象,用于在SQL中处理(SELECT的)查询结果集。它允许逐行地访问查询结果集的数据,以进行一系列操作,如更新、删除或插入数据。游标通常用于存储过程或触发器中,用于对数据进行逐行处理。通过游标,可以实现对结果集的逐行处......
  • 深入理解正则表达式:从入门到精通
    title:深入理解正则表达式:从入门到精通date:2024/4/3018:37:21updated:2024/4/3018:37:21tags:正则Python文本分析日志挖掘数据清洗模式匹配工具推荐第一章:正则表达式入门介绍正则表达式的基本概念和语法正则表达式是一种用于描述字符串模式的表达式,由普通......
  • SQL SERVER 从入门到精通 第5版 第三篇 高级应用 第11章 触发器 读书笔记
     第11章触发器>.概述触发器是一种特殊类型的存储过程.当指定表中的数据发生变化时触发器自动生效.它与表紧密相连,可以看作表定义的一部分.触发器不能通过名称被直接调用,更不允许设置参数.在SQLSERVER中,一张表可以有多个触发器.用户可以使用INS......
  • Linux 根文件系统的移植(从入门到精通)
    一、简介提到操作系统的安装,还得从大学的时候说起,刚入学的时,朋友的系统本崩了,跑去电脑城换个系统花了40大洋,震惊了贫穷的我。好像发现了商机,果断开始了折腾自己的电脑,然后用朋友的电脑进行测试,由于对启动项不了解,有次蹦了过后,自己花钱去维修电脑哪里安装了一次,偷偷的学习。在这样......
  • Docker从入门到精通:Docker 容器数据卷详解
    前言在Docker中,数据卷(Volume)是一种可用于持久化存储数据的重要机制。它允许容器与宿主机之间或者容器之间共享数据,并且能够保留数据,即使容器被删除也不会丢失。本文将介绍Docker容器数据卷的基本概念、用法以及一些实用技巧。什么是Docker容器数据卷?Docker容器数据卷是......
  • Docker从入门到精通:Docker log 命令学习
    前言在使用Docker进行应用程序容器化的过程中,日志管理是一个至关重要的方面。通过有效地管理容器的日志,我们可以快速诊断问题、监视应用程序的运行状况以及确保系统的健康运行。Docker提供了强大的日志管理功能,而dockerlog命令则是我们在日常工作中经常使用的工具之一。本......
  • 30 天精通 RxJS (31):如何 Debug?
    Debug一直是RxJS的难题,原因是当我们使用RxJS后,代码就会变得高度抽象化;实际上抽象并不是什么坏事,抽象会让代码显得简洁、干净,但同时也带来了除错上的困难。在编写程序时,我们都会希望代码是简洁且可读的。但当我们用简洁的代码来处理复杂的问题,就表示我们的代码会变得......
  • 30 天精通 RxJS (30):Cold & Hot Observable
    HotObservable跟ColdObservable的差别,其实就是资料源(DataSource)在Observable内部建立还是外部建立。在RxJS中很常会看到ColdObservable跟HotObservable这两个名词,其实他们是在区分不同行为的Observable,所谓的ColdObservable就是指每次订阅都是独立的......
  • 30 天精通 RxJS (29):30 天感言
    30天悄悄的就过了,这30篇的文章基本上已经把RxJS一个核心三个重点(Observable+Observer+Subject+Scheduler)以及各个operators几乎也都有写到。最开始写这个系列的文章是希望能让RxJS的学习曲线降低,所以文章的前后顺序及内容都是特别规划过的,不知道我到底是不是......
  • 30 天精通 RxJS (28):Scheduler 基本观念
    不晓得读者们还记不记得,我们在前面的文章中有提到Scheduler是为了解决RxJS衍生的最后一个问题,而我们现在就在揭晓这个谜底。其实RxJS用久了之后就会发现Observable有一个优势是可以同时处理同步和非同步行为,但这个优势也带来了一个问题,就是我们常常会搞不清处现在的......