精通以太坊(三)

274 阅读1小时+

精通以太坊(三)

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

译者:飞龙

协议:CC BY-NC-SA 4.0

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

在本章中,您将了解到关于以太坊开发者可用的几个重要工具,用于创建强大的分布式应用和可为数百万潜在用户提供安全服务的智能合约。以太坊开发世界充满了许多有用的工具,旨在让您在创建复杂的分布式应用和智能合约时更轻松,这些应用和智能合约使用了 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/we…

  • NEthereum:这是一款类似于 web3.js 的智能合约通讯工具,专为.NET 开发人员而设计。那些使用流行的.NET 库并喜欢使用 C#编程的人会喜欢这个工具,因为它专为这些程序员而制作。它为你提供了连接现有.NET 环境与你的 Web 应用程序所需的一切,包括库和客户端集成。你可以在这里了解更多关于 NEthereum 的信息:nethereum.com

  • Drizzle:这是一个为你的 dApps 提供 Redux 集成的工具,可以轻松管理状态和数据存储。如果你熟悉 Redux 和 React,你会喜欢这个工具,因为它为你提供了一个干净的库,可以轻松实现 Redux 在你的 dApps 上的好处。由 Truffle 的创建者制作,非常适合大型项目。你可以在这里获取它:github.com/trufflesuit…

以太坊后端 API

大多数以太坊应用程序都需要某种形式的中心化后端来执行一些对智能合约来说不可行的任务,要么是因为超出了合同的能力,要么是因为有更好的处理某些操作的方法。在这些情况下,我们仍然需要与区块链进行通信。这就是后端 API 出场的地方:帮助我们创建工具和系统,改善我们总体的 dApps 和智能合约。

  • Web3.py:这是用于 Python 的流行的以太坊实现,可以为这种流行的语言创建工具和系统。Python 与 web3.js 非常搭配,因为你可以创建高效的脚本来自动执行一些操作,比如检查智能合约事件。其语法与原始的web3.js非常相似,因此你会感到很舒适使用它。在这里查看它:github.com/Ethereum/we…

  • Eventeum:这是一个用于与后端微服务通信智能合约事件的工具。如果您对微服务不熟悉,它们只是专注于以非常高效和可维护的方式执行某些特定任务的小型应用程序,以便最终应用程序非常高效,并且易于使用模块进行优化和替换。Eventeum 正在与这些微服务一起工作,以处理您智能合约生成的以太坊事件,以便您可以轻松地在复杂的服务网络上实施事件。它适用于 Java,非常适合希望实现可伸缩性的后端开发人员。在这里查看它:github.com/ConsenSys/e…

应用二进制接口工具

应用二进制接口ABIs)是描述智能合约函数、变量、修饰符以及所有其他内容的 JSON 对象。ABIs 的目标是帮助您的 dApp 快速理解智能合约,以便知道哪些功能对您可用。现在,重要的是尽可能多地利用这一协议,因为您将在所有 dApp 中使用它。以下是一些工具,可以帮助您真正提高对 ABI 的理解:

  • Abi-decoder:这是一个小巧的 JavaScript 工具,允许您解码通常加密并难以理解的复杂交易对象。您知道,每当您通过执行智能合约中的函数发送交易时,您都在与加密数据的区块链进行交互。迟早,您都需要阅读这些交易,无论是因为您正在调试您的 dApp,还是因为您需要出于其他原因了解其中发生了什么。使用 abi-decoder,您可以解码交易数据和交易日志,这对数据高效的 dApp 来说太棒了。在 github.com/ConsenSys/a… 了解更多关于这个由 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/Et… 了解更多信息。

  • Harmony:由 EtherCamp 制作,这是以太坊早期的一个热门网站,提供了他们自己的 IDE 和工具。Harmony 是用 JavaScript 和 Java 的组合编写的,并基于 EthereumJ。他们的目标是提供一个清晰的 Web 仪表板界面,让你可以轻松监控和详细分析区块链。它非常适用于需要深入了解底层运行情况的项目。请访问 github.com/ether-camp/… 了解更多信息。

以太坊存储

当我们谈及存储时,指的是在分散的云中保存各种文件,这些云可能与以太坊区块链合作,也可能没有。这些是允许您存储合同和文件的应用程序,而无需依赖于集中式服务器:

  • 星际文件系统IPFS):这是分散式存储的最知名实现,它允许您将大型文件存储在连接的节点的分散网络中,而不是将您的信息存储在集中式服务器和数据库中。它正在被用于各种项目中,这些项目希望利用完全分散的应用程序的可能性,在这些应用程序中,没有集中的故障点。这些类型的项目将来会被广泛使用,因为它们在数千个节点在高质量网络上复制信息时要安全得多。IPFS 介于 Torrent 和 Git 协议之间,其中文件根据其内容进行标识。因此,具有相同内容的两个相同文件将具有相同的加密标识符,称为哈希。这是一个非常重要的革命,因为它们删除了重复的文件,增加了数据的可用性,并允许更好地利用资源,因为许多节点将共享相同的信息,而不是分开的。在他们的网站ipfs.io上查看它。

  • Swarm:这是建立在以太坊之上的一个协议,其目标是以分散的方式共享文件,就像 IPFS 一样,但无需依赖外部团队。它由核心以太坊团队不断改进,并与整个系统无缝集成,因此您可以将其与您的 dApps 和智能合约集成,而无需头痛。在他们的网站swarm-gateways.net上查看它。

以太坊消息传递

以太坊消息传递意味着在对等方之间直接交换加密信息,而无需中介,以便您几乎实时地获得信息。它们是速度优先的聊天和应用程序的绝佳工具:

  • Whisper:最知名的实现对等消息传递的协议,它建立在以太坊之上,并与其核心的所有系统完全集成。您可以使用它与其他 dApps 进行通信,配置最小。智能合约是相互连接的,有一个层次,它们可以安全地共享信息。在这里了解更多关于 Whisper 的信息:github.com/Ethereum/wi…

  • Devp2p:这是建立在以太坊之上的另一个协议,允许用户和 dApps 在不必创建缓慢的区块链交易的情况下以高速交换加密消息。有一个用 Python 编写的实现,称为 pydevp2p,它为您提供了一个简单的界面,以便在您的应用程序中包含消息传递,以便人们可以开始相互交换数据。在他们的官方 GitHub 页面上了解更多信息:github.com/Ethereum/de…

以太坊消息应用程序为我们提供了丰富的可能性集,可以创建更好的 dApps 和先进的智能合约,所以当你想要在以太坊上开发基于用户的游戏或聊天应用程序时,你应该关注这些服务。

学习以太坊测试和保护工具

区块链安全是任何其他特性之上的首要任务。没有安全的应用程序,我们无法着手处理最简单的智能合约,因为用户不会信任我们的代码。这就是为什么作为一名精通以太坊开发者,你必须了解如何确保程序安全的所有可能性。

理解监控工具

监控是观察你的应用程序在现实世界中的行为的行为。你知道,监视你的代码非常重要,因为它可能被全世界成千上万的用户使用:你不希望他们在随机时刻有糟糕的体验。确保查看这些工具,以提升你的智能合约水平,因为它们可以极大地提高你的应用程序质量:

  • 智能合约监视器: 由 Neufund 制作,这是一家致力于创建法律约束力智能合约等其他事物的公司。该工具允许你监视你的智能合约活动,并查看它可能导致问题的地方。你可以将其用作自己的定制区块浏览器,作为当你的应用程序中的资金严重减少时的安全工具,或者用于任何需要仔细监视的情况。它可以从终端简单使用,并具有简单的输出界面,以查看发生了什么。在此处了解更多信息:github.com/Neufund/sma…

  • 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/raineorshin…

理解审计工具

审计是手动查看代码各个不同部分的过程,以使用诸如逐行分析、漏洞测试和黑客路径等流程找出潜在的漏洞。你必须熟悉它们以保证可持续、高质量的代码项目。

请注意,它们的目标是加快你的审计流程,因此它们更像是一套深思熟虑的过程的辅助工具:

  • EthSum: 这是一个简单直接的工具,由 Netlify 制作,允许你对以太坊地址进行校验和。有时,你需要对地址进行校验和,以确保它是一个经过正确创建的良好形式的地址。它主要用于 Truffle 项目,其中你必须为你的项目拥有有效的地址,所以 EthSum 是一个不错的辅助工具来验证地址。你可以在这里获取它:ethsum.netlify.com

  • Decode:这是一个使交易数据易于为您的 testrpc 节点理解的工具。当您审核项目时,您必须运行几个测试和手动检查以验证结果的完整性,而大多数情况下很难在 testrpc 或类似的测试环境中执行此操作,因为生成的数据令人困惑。Decode 通过使交易易于阅读和理解来解决了这个问题。了解更多信息,请访问:github.com/dteiml/deco…

  • 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天,并在您添加100 的服务,免费使用 60 天,并在您添加 25 到服务时额外获得 25。这将足以至少运行一个基本的VPS服务器3个月。您需要添加信用卡/借记卡或添加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 root@socialmusic.online

它将与以前完全相同的方式工作。您还可以在端口 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。