跳转至

如何用以太坊、Web3.js & Solidity 智能合约搭建区块链 App

原文:https://www.dappuniversity.com/articles/how-to-build-a-blockchain-app

作者格雷戈里·麦卡宾2022 年 12 月 1 日

大家好,我是来自 Dapp 大学的Gregory

我将向您展示如何使用 Ethereum、Web3.js 和 Solidity smart contracts 创建您的第一个区块链应用程序。你不需要了解区块链的任何事情。我从头教你。使用这个包含代码示例和书面说明的分步指南,立即开始您的区块链开发者之旅!

https://www.youtube.com/embed/VH9Q2lf2mNo

JOIN MY FREE TRAINING! *

在上面 3 小时的视频中,我将带您构建一个完整的区块链应用程序。您也可以按照本指南中的逐步说明进行操作。如果你已经知道了基础知识,你可以跳过这一步,现在就开始编写应用程序!

目录

我们要建造什么?

今天,我将通过构建一个由区块链支持的完整应用程序,从头开始教你区块链编程。

我们将创建:

  1. 以太坊智能合约与 Solidity 编程语言
  2. 我们将用 JavaScript 为智能合约编写测试
  3. 我们将智能合同部署到区块链
  4. 我们将使用 Web3.js 和 React.js 创建一个客户端网站,以便用户可以与智能合同对话

如果你不懂这些语言,不要担心,我会边学边教!

让我们看看我们将要构建的应用程序:

Blockchain App Demo Gif

这是一个在区块链上运行的市场,有点像 Craigslist。它允许人们列出待售商品,但与 Craigslist 不同的是,它还允许人们在网站上用加密货币购买商品。

Maketplace Blockchain App Diagram

每当有人购买该物品,他们立即成为所有者。这是因为该应用程序由区块链上管理市场的智能合约提供支持。它跟踪谁拥有待售商品,每当有人用加密货币购买商品时,它就会自动转移商品的所有权。它像自动售货机一样工作。

让我把它分解一下,以防有什么混淆。我来回答几个基本问题。

什么是区块链?

区块链是一个点对点的节点网络,所有节点都可以相互通信。这些节点本质上是分担类似于 web 服务器的职责的计算机,例如运行程序和存储数据,只要连接到区块链就可以访问这些数据。所有节点一起工作,创建一个任何人都可以连接的公共网络。

你可以从几个方面来思考区块链:

  • 一台巨大的世界范围的计算机由协同工作的小型计算机(节点)组成。
  • 任何人都可以连接到的网络,用于汇款、运行程序等...
  • 一个允许你存储和检索信息的数据库。

区块链承担 web 服务器通常承担的责任:

  • 运行应用程序代码
  • 托管数据库

Blockchain Node Diagram

然而,区块链工作方式不同于传统的 web 服务器。区块链上的所有代码和数据都是分散的,即它分布在网络中的所有节点上。它不位于中央网络服务器上。

所有这些数据都包含在名为数据块的记录包中,这些数据块 链接在一起 组成公共总账。网络上的所有节点都参与确保这些数据保持安全和不变。这就是区块链如此强大的原因!

那么,你如何连接到区块链并使用它呢?

由于这是一个节点网络,您可以运行自己的节点,或者连接到一个节点。一旦你这样做了,你就能获得区块链的全部力量!

什么是智能合同?

智能合约是我们用来创建区块链应用程序的构件。它们是我们可以用源代码编写并部署到区块链的程序。它们是用 Solidity 编程语言编写的,我将在本教程中教你。

智能合约是不可变的,这意味着一旦它们被创建就不能改变!一旦智能合约被部署到区块链,其代码就不能像普通应用程序一样被更新。这是因为它代表了一个数字合同或协议。毕竟,您不希望任何其他真实世界的合同在创建后发生变化,对吗?

你可以把智能合约想象成网络上的微服务或 API。如果公共分类帐就像一个数据库,那么智能契约就是读取、写入和执行业务逻辑的层。

智能合同是如何工作的?

在本教程中,我们将编写智能合同来推动市场。它的工作方式有点像自动售货机。

Maketplace Blockchain App Diagram

任何时候有人在市场上购买物品,他们都会向智能合约发送以太坊加密货币。智能合同将像自动售货机一样工作,向买家分发商品,并将加密货币支付即时转移到卖家。

区块链 App 是如何工作的?

为了理解区块链应用程序是如何工作的,让我们先来看看一个普通的 web 应用程序是如何工作的。

Web Application Diagram

通常,对于 web 应用程序,您可以使用 web 浏览器访问网页。该网站的所有 HTML、CSS 和 JavaScript 代码都位于一个中央网络服务器上,并与后端(用任何编程语言编写)对话,后端也与数据库对话。

我们将在本教程中构建的区块链应用程序的工作方式不同。

Blockchain App Diagram

我们将通过一个特殊的 web 浏览器访问我们的区块链应用程序,该浏览器与一个用 HTML、CSS 和 JavaScript 编写的前端网站进行对话。该网站将直接与区块链对话,而不是与后端 web 服务器对话。区块链本质上将是我们的后端,为我们分散的市场托管所有的代码和数据。

现在我们已经介绍了基础知识,让我们开始构建我们的项目吧!

安装依赖项

教程这一部分的视频片段从 5:49 开始。

现在让我们安装构建项目所需的所有依赖项。首先,我们将建立一个区块链人来本地开发应用程序。

Ganache 个人区块链

这种依赖性是一种个人区块链,是一种本地开发区块链,可用于模仿公共区块链的行为。我推荐使用 Ganache 作为你以太坊开发的个人区块链。它将允许您部署智能合约、开发应用程序和运行测试。它可以作为桌面应用程序和命令行工具在 Windows、Mac 和 Linux 上使用!

Ganache Personal Blockchain Application

在本教程中,我将指导您设置桌面应用程序。你可以在这里找到你的操作系统的最新版本。一旦下载了存档的包,解压安装程序并运行安装步骤。安装完成后,无论何时打开它,您都会看到以下屏幕:

Ganache Personal Blockchain Application

耶!🎉现在你有一个个人区块链网络运行!您可以看到运行 Ganache 的服务器的一些详细信息,以及连接到网络的帐户列表。每个账户都被记入 100 英镑。这是一个巨大的时间节省!如果你打算从头开始创建你自己的个人区块链网络,或者在一个测试网络上开发你的应用程序,你将不得不手动创建所有 10 个帐户,并向每个帐户存入乙醚。谢天谢地,Ganache 已经为我们做了这件事,所以我们不必担心它。

节点。射流研究…

既然您已经运行了私有区块链,那么您需要配置您的环境来开发智能合约。您需要的第一个依赖项是 Node.js 附带的节点包管理器或 NPM。您可以查看是否已经安装了 Node,方法是在终端上键入:

$ node -v

如果你还没有安装 node,你可以访问 Node.js 网站下载。

块菌框架

现在让我们安装 Truffle 框架,它提供了一套用 Solidity 编程语言开发以太坊智能联系人的工具。

Truffle Blockchain Smart Contract Development Framework

以下是我们将从 Truffle 框架中获得的所有功能的概述:

  • 智能合同管理 -用 Solidity 编程语言编写智能合同,并将它们编译成运行在以太坊虚拟机(EVM)上的字节码。
  • 自动化测试(Automated Testing)——针对你的智能合约编写测试,以确保它们按照你想要的方式运行。这些测试可以用 JavaScript 或 Solidity 编写,可以在 Truffle 配置的任何网络上运行,包括公共区块链网络。
  • 部署&迁移 -编写脚本将智能合约迁移和部署到任何公共以太坊区块链网络。
  • 网络管理 -连接到任何公共以太坊区块链网络,以及任何你可能用于开发目的的个人区块链网络。
  • 开发控制台——使用 Truffle 控制台在 JavaScript 运行时环境中与智能合约进行交互。为此,您可以连接到您在网络配置中指定的任何区块链网络。
  • 用 JavaScript 编写可以在区块链公共网络上运行的定制脚本。您可以在这个文件中编写任意代码,并在项目中运行它。
  • 客户端开发——配置您的 truffle 项目来托管客户端应用程序,这些应用程序与部署到区块链的智能合约进行对话。

你可以像这样在命令行中安装带有 NPM 的松露。注意:为了跟随本教程,使用下面指定的松露的精确版本是很重要的。

$ npm install -g [[email protected]](/cdn-cgi/l/email-protection)
Metamask 以太坊钱包

现在是时候把你的网络浏览器变成区块链浏览器了。大多数主要的网络浏览器目前都没有连接到区块链网络,所以我们必须安装一个浏览器扩展来允许他们这样做。

Metamask Ethereum Wallet Browser Extension

我将为谷歌浏览器扩展元掩码。要安装 Metamask,请访问此链接或在谷歌 Chrome 网络商店搜索 Metamask Chrome 插件。一旦你安装了它,确保它在你的扩展列表中被选中。安装 Chrome 浏览器后,你会在浏览器的右上角看到狐狸图标。如果遇到困难,请参考视频演示!

Metamask 还允许我们在连接到区块链时管理我们的个人账户,以及管理我们需要支付交易的以太网资金。

第 1 部分:项目设置

教程这一部分的视频片段从 8:40 开始。首先,确保您已经安装了上一节中的所有依赖项。

让我们快速开始构建我们的应用程序吧!我们将使用我的初学者工具包,而不是手动设置项目。通过克隆初学者工具包创建您的项目,如下所示:

$ git clone https://github.com/dappuniversity/starter_kit marketplace

现在,像这样进入刚刚克隆的目录:

$ cd marketplace

🎉维奥拉。您已经立即建立了您的项目。让我们来看看我们刚刚创建的内容:

Blockchain App File Structure

这是一个自定义的 truffle 项目,我已经创建来帮助您快速开始构建全栈区块链应用程序!您可以创建智能合约,测试它们,并使用这个项目中的 Web3.jsReact.jsBootstrap 构建前端 web 应用程序。

package.json文件中已经有了您需要的所有依赖项:

{
  "name": "eth-marketplace",
  "version": "0.1.0",
  "description": "An Ethereum Marketplace",
  "author": "[[email protected]](/cdn-cgi/l/email-protection)",
  "dependencies": {
    "babel-polyfill": "6.26.0",
    "babel-preset-env": "1.7.0",
    "babel-preset-es2015": "6.24.1",
    "babel-preset-stage-2": "6.24.1",
    "babel-preset-stage-3": "6.24.1",
    "babel-register": "6.26.0",
    "bootstrap": "4.3.1",
    "chai": "4.2.0",
    "chai-as-promised": "7.1.1",
    "chai-bignumber": "3.0.0",
    "react": "16.8.4",
    "react-bootstrap": "1.0.0-beta.5",
    "react-dom": "16.8.4",
    "react-scripts": "2.1.3",
    "truffle": "5.0.5",
    "web3": "1.0.0-beta.55"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },
  "eslintConfig": {
    "extends": "react-app"
  },
  "browserslist": [
    ">0.2%",
    "not dead",
    "not ie <= 11",
    "not op_mini all"
  ]
} 

您可以像这样继续安装您的依赖项:

$ npm install

接下来让我们看看truffle-config.js文件:

require('babel-register');
require('babel-polyfill');

module.exports = {
  networks: {
    development: {
      host: "127.0.0.1",
      port: 7545,
      network_id: "*" // Match any network id
    },
  },
  contracts_directory: './src/contracts/',
  contracts_build_directory: './src/abis/',
  compilers: {
    solc: {
      optimizer: {
        enabled: true,
        runs: 200
      }
    }
  }
} 

该文件的主要职责是将我们的项目连接到区块链网络。我已经设置好连接到我们的 Ganache 个人区块链,即127.0.0.1:7545。我还会提到,我已经将这个项目的结构配置为不同于 Truffle 的默认文件结构。我已经将智能合同移动到了src目录,这样 react 应用程序就可以访问它们。你可以在contracts_directorycontracts_build_directory设置下看到那个配置。

现在我们已经看到了项目结构,让我们通过在 contracts 目录中创建一个新文件来开始编写我们的智能合约:

$ touch src/contracts/Marketplace.sol

在这个文件中,让我们开始编写我们的智能契约可靠性编程语言:

pragma solidity ^0.5.0;

contract Marketplace {

}

首先,我们首先声明我们想要使用的 Solidity 编程语言的版本。接下来,我们声明我们的智能契约Marketplace。我们将在花括号内添加所有智能合约代码。让我们这样做:

pragma solidity ^0.5.0;

contract Marketplace {
    string public name;

}

该代码创建了一个“状态变量”,其值将存储在区块链上。我们将调用变量name,因为我们将使用它来存储智能合约的名称(仅用于测试目的)。由于 Solidity 是一种静态类型的编程语言,我们必须在声明变量之前声明string数据类型。最后,我们声明状态变量public,这样我们就可以在智能契约之外读取它的值,我们马上就会这么做。

接下来,让我们这样设置这个变量的值:

pragma solidity ^0.5.0;

contract Marketplace {
    string public name;

    constructor() public {
        name = "Dapp University Marketplace";
    }
}

我们在constructor函数中指定name的值。这是一个特殊的函数,每当首次创建智能合约时,即部署到区块链时,都会调用该函数。无论何时部署,它都会将name的值设置为我们在这里指定的字符串。

现在,让我们编译智能合同,以确保一切正常:

$ truffle compile

终端中的输出将显示编译后的智能合约的位置!

接下来,让我们将 mart 契约部署到我们的 Ganache 个人 blockhain。为此,创建一个新的迁移文件,如下所示:

$ touch migrations/2_deploy_contracts.js

这个文件告诉 Truffle 将我们的智能合同部署到区块链。如果你熟悉的话,这有点像传统数据库的迁移文件。另外,请注意,迁移文件是有编号的,以便 Truffle 知道以什么顺序运行它们。

将此代码输入到新创建的迁移中:

const Marketplace = artifacts.require("Marketplace");

module.exports = function(deployer) {
  deployer.deploy(Marketplace);
};

现在迁移像这样运行迁移:

$ truffle migrate

现在,我们可以从松露控制台检查我们的智能合同。您可以从命令行启动 Truffle 控制台,如下所示:

$ truffle console

现在,我们可以在控制台中使用 JavaScript 获得智能合约的部署副本,如下所示:

marketplace = await Marketplace.deployed()

你的主机可能会返回undefined,但没关系!您可以通过再次键入变量名来获取智能协定的值,如下所示:

marketplace

您可以在区块链上获得智能合约的地址,如下所示:

marketplace.address

接下来,我们来读一下名字:

name = await marketplace.name()

然后:

name

耶!🎉您已经成功设置了项目,并将基本智能合约部署到区块链!

现在让我们为智能合约编写一个测试。测试智能合约非常重要,因为在区块链上线之前,你需要确保它们能够完美运行。请记住,一旦您部署了它们,它们就无法更改!您只能重新部署新的副本。

为智能合约测试创建一个新文件,如下所示:

$ mkdir test
$ touch test/Marketplace.test.js 

在该文件中,使用以下代码:

const Marketplace = artifacts.require('./Marketplace.sol')

contract('Marketplace', (accounts) => {
  let marketplace

  before(async () => {
    marketplace = await Marketplace.deployed()
  })

  describe('deployment', async () => {
    it('deploys successfully', async () => {
      const address = await marketplace.address
      assert.notEqual(address, 0x0)
      assert.notEqual(address, '')
      assert.notEqual(address, null)
      assert.notEqual(address, undefined)
    })

    it('has a name', async () => {
      const name = await marketplace.name()
      assert.equal(name, 'Dapp University Marketplace')
    })

  })
})

我来解释一下这个测试。我们在这个文件中用 Javascript 编写了所有的测试,使用了Mocha 测试框架柴断言库。这些都与 Truffle 框架捆绑在一起。我们将使用 JavaScript 编写所有这些测试来模拟客户端与智能合约的交互,就像我们在控制台中所做的一样。

这个测试做两件事:

  • 检查智能合约是否有地址,即它是否已成功部署到网络。
  • 检查部署时是否设置了名称。

您可以观看视频以获得对该测试文件的更深入的解释。

现在,让我们从命令行运行测试,如下所示:

$ truffle test

🎉耶,他们通过了!

干得好!如果你卡住了,请随意再看一遍这部分视频,以便进一步澄清。你也可以在这里找到所有代码的副本

第 2 部分:销售产品

这部分教程的视频片段从 33:10 开始。你也可以在这里找到的代码

现在,让我们继续构建市场智能合约。我们将创建第一个功能,它将允许用户列出市场中的待售商品。为了做到这一点,我们需要用这样的结构对产品建模:

struct Product {
    uint id;
    string name;
    uint price;
    address owner;
    bool purchased;
}

Solidity 允许你创建你自己的数据结构,具有任意属性。这正是我们通过创建一个Product结构所做的。它存储了我们需要的产品的所有属性,比如idnamepriceownerpurchased

接下来,我们需要一个地方来储存区块链上的产品。我们将创建一个实度映射,如下所示:

mapping(uint => Product) public products;

映射的工作方式类似于带有键值对的关联数组或哈希表。映射具有返回唯一值的唯一键。在我们的例子中,我们将使用一个id作为键,值将是一个Product结构。这将基本上允许我们通过 id 查找产品,就像数据库一样。

接下来,我们希望使用如下所示的productCount计数器缓存来跟踪智能合同中有多少产品:

uint public productCount = 0;

我们使用计数器缓存,因为没有办法知道映射中有多少产品。你不能在 Solidiy 中检查贴图的“长度”或“大小”。因此,如果我们想获取所有的产品,我们必须逐个读出它们。我们将使用计数器缓存来确定这样做的次数。

此时,您的智能合约应该如下所示:

pragma solidity ^0.5.0;

contract Marketplace {
    string public name;
    uint public productCount = 0;
    mapping(uint => Product) public products;

    struct Product {
        uint id;
        string name;
        uint price;
        address owner;
        bool purchased;
    }

    constructor() public {
        name = "Dapp University Marketplace";
    }

}

接下来,让我们创建一个函数来创建新产品。这个函数将做几件事:

  • 用结构创建新产品
  • 将该结构添加到映射中,并将其存储在区块链上
  • 触发一个事件,让某人知道一个产品被创造了

我们可以像这样创建函数:

function createProduct(string memory _name, uint _price) public {
    // Require a valid name
    require(bytes(_name).length > 0);
    // Require a valid price
    require(_price > 0);
    // Increment product count
    productCount ++;
    // Create the product
    products[productCount] = Product(productCount, _name, _price, msg.sender, false);
    // Trigger an event
    emit ProductCreated(productCount, _name, _price, msg.sender, false);
}

让我解释一下这段代码:

  • 首先,我们创建一个接受

    js name

    js price

    参数的函数。产品价格用以太的最小细分单位魏表示(完整解释见视频)。 * 接下来,我们添加一些在函数继续执行之前必须满足的要求。我们检查名称是否存在,价格是否大于 0。 * 接下来,我们通过使用++操作符增加产品计数来生成一个新的产品 id(这只是将前一个值加 1)。 * 然后,我们创建一个新产品并将其添加到映射中。注意msg.sender是创建产品的用户的地址。 * 最后,我们触发一个事件,让每个人都知道产品已经成功创建。

现在让我们添加事件定义,以便可以触发它:

event ProductCreated(
    uint id,
    string name,
    uint price,
    address owner,
    bool purchased
);

外部订阅者可以侦听此事件,以验证产品是在区块链上创建的。我们将在智能合约测试中检查该事件。

现在,该部分的完整智能合约代码应该如下所示:

pragma solidity ^0.5.0;

contract Marketplace {
    string public name;
    uint public productCount = 0;
    mapping(uint => Product) public products;

    struct Product {
        uint id;
        string name;
        uint price;
        address owner;
        bool purchased;
    }

    event ProductCreated(
        uint id,
        string name,
        uint price,
        address owner,
        bool purchased
    );

    constructor() public {
        name = "Dapp University Marketplace";
    }

    function createProduct(string memory _name, uint _price) public {
        // Require a valid name
        require(bytes(_name).length > 0);
        // Require a valid price
        require(_price > 0);
        // Increment product count
        productCount ++;
        // Create the product
        products[productCount] = Product(productCount, _name, _price, msg.sender, false);
        // Trigger an event
        emit ProductCreated(productCount, _name, _price, msg.sender, false);
    }
}

现在让我们添加一些测试来确保这个函数正常工作。在测试文件中使用以下代码:

const Marketplace = artifacts.require('./Marketplace.sol')

require('chai')
  .use(require('chai-as-promised'))
  .should()

contract('Marketplace', ([deployer, seller, buyer]) => {
  let marketplace

  before(async () => {
    marketplace = await Marketplace.deployed()
  })

  describe('deployment', async () => {
    it('deploys successfully', async () => {
      const address = await marketplace.address
      assert.notEqual(address, 0x0)
      assert.notEqual(address, '')
      assert.notEqual(address, null)
      assert.notEqual(address, undefined)
    })

    it('has a name', async () => {
      const name = await marketplace.name()
      assert.equal(name, 'Dapp University Marketplace')
    })
  })

  describe('products', async () => {
    let result, productCount

    before(async () => {
      result = await marketplace.createProduct('iPhone X', web3.utils.toWei('1', 'Ether'), { from: seller })
      productCount = await marketplace.productCount()
    })

    it('creates products', async () => {
      // SUCCESS
      assert.equal(productCount, 1)
      const event = result.logs[0].args
      assert.equal(event.id.toNumber(), productCount.toNumber(), 'id is correct')
      assert.equal(event.name, 'iPhone X', 'name is correct')
      assert.equal(event.price, '1000000000000000000', 'price is correct')
      assert.equal(event.owner, seller, 'owner is correct')
      assert.equal(event.purchased, false, 'purchased is correct')

      // FAILURE: Product must have a name
      await await marketplace.createProduct('', web3.utils.toWei('1', 'Ether'), { from: seller }).should.be.rejected;
      // FAILURE: Product must have a price
      await await marketplace.createProduct('iPhone X', 0, { from: seller }).should.be.rejected;
    })
  })
})

让我们检查每个新零件。首先,我们像这样在测试套件中添加一些额外的工具。我们已经在我们的包中安装了这些。

require('chai')
.use(require('chai-as-promised'))
.should() 

接下来,向测试场景添加 3 个新帐户,deployersellerbuyer:

contract('Marketplace', ([deployer, seller, buyer]) => {

然后,我们为创建产品创建一个新的测试示例:

describe('products', async () => {
  let result, productCount

  before(async () => {
    result = await marketplace.createProduct('iPhone X', web3.utils.toWei('1', 'Ether'), { from: seller })
    productCount = await marketplace.productCount()
  })

  //... 

这用一个before钩子设置了测试示例,在每个测试运行之前创建一个产品。最后,我们为产品创建创建一个完整的测试,如下所示:

it('creates products', async () => {
  // SUCCESS
  assert.equal(productCount, 1)
  const event = result.logs[0].args
  assert.equal(event.id.toNumber(), productCount.toNumber(), 'id is correct')
  assert.equal(event.name, 'iPhone X', 'name is correct')
  assert.equal(event.price, '1000000000000000000', 'price is correct')
  assert.equal(event.owner, seller, 'owner is correct')
  assert.equal(event.purchased, false, 'purchased is correct')

  // FAILURE: Product must have a name
  await await marketplace.createProduct('', web3.utils.toWei('1', 'Ether'), { from: seller }).should.be.rejected;
  // FAILURE: Product must have a price
  await await marketplace.createProduct('iPhone X', 0, { from: seller }).should.be.rejected;
})

首先,我们检查创建一个产品是否使产品数量增加了 1。然后,我们检查智能合约事件日志,以查看产品是用正确的值创建的。我们像这样深入研究事件日志:result.logs[0].args(尝试将这些值记录到控制台,看看它们是什么样子)。我们检查所有这些值是否正确,如 id、姓名、价格等...

最后,我们检查失败案例。例如,如果没有名字,或者如果价格小于或等于 0,我们确保函数调用失败。

干得好!如果你卡住了,请随意再看一遍这部分视频,以便进一步澄清。你也可以在这里找到所有代码的副本

第 3 部分:购买产品

本教程这一部分的视频片段从 01:01:07 开始。你也可以在这里找到的代码

现在让我们创建一个购买产品的函数。每当有人调用这个函数时,他们将提交他们想要购买的产品的id(这将由我们的客户端应用程序处理)。此外,当他们调用该功能时,他们将从钱包中发送以太坊加密货币来购买产品。我们将像这样设置函数:

function purchaseProduct(uint _id) public payable {
  // ...
}

注意,我们已经做了这个函数payable,这意味着它将接受 Etherum 加密货币。我们很快就会看到这一点。因为我们要向所有者付费,所以我们必须更新现有的结构和事件来使用address payable类型,如下所示:

struct Product {
    uint id;
    string name;
    uint price;
    address payable owner;
    bool purchased;
}
event ProductCreated(
    uint id,
    string name,
    uint price,
    address payable owner,
    bool purchased
);

现在让我们填写函数内部的代码:

function purchaseProduct(uint _id) public payable {
    // Fetch the product
    Product memory _product = products[_id];
    // Fetch the owner
    address payable _seller = _product.owner;
    // Make sure the product has a valid id
    require(_product.id > 0 && _product.id <= productCount);
    // Require that there is enough Ether in the transaction
    require(msg.value >= _product.price);
    // Require that the product has not been purchased already
    require(!_product.purchased);
    // Require that the buyer is not the seller
    require(_seller != msg.sender);
    // Transfer ownership to the buyer
    _product.owner = msg.sender;
    // Mark as purchased
    _product.purchased = true;
    // Update the product
    products[_id] = _product;
    // Pay the seller by sending them Ether
    address(_seller).transfer(msg.value);
    // Trigger an event
    emit ProductPurchased(productCount, _product.name, _product.price, msg.sender, true);
}

让我解释一下这个函数的作用。

  • 首先,我们从映射中获取产品,并在内存中创建它的新副本。
  • 然后,我们将当前所有者存储到一个变量中。我们将在这个函数中转移产品的所有权,所以我们需要知道最初的所有者是谁。
  • 接下来,我们添加一些需求。我们检查有效的 id,交易中有足够的以太网加密货币,买方不是卖方,并且产品尚未购买。
  • 然后我们促成交易。我们将所有权转移给买方,将产品标记为已购买,并将产品添加回映射。之后,我们将加密货币付款发送给卖家。请注意,以太加密货币金额是用函数的元数据来跟踪的。我们可以用全局变量msg.sender来访问这个值。同样,该值用 Wei 表示(进一步解释见视频)。
  • 最后,我们触发一个事件来声明产品已经成功创建。

现在让我们为产品销售创建一个新事件。它的工作方式与我们在上一节中创建的事件几乎完全一样,只是名称不同:

event ProductPurchased(
    uint id,
    string name,
    uint price,
    address payable owner,
    bool purchased
);

现在让我们为purchaseProduct函数添加一个测试,如下所示:

it('sells products', async () => {
  // Track the seller balance before purchase
  let oldSellerBalance
  oldSellerBalance = await web3.eth.getBalance(seller)
  oldSellerBalance = new web3.utils.BN(oldSellerBalance)

  // SUCCESS: Buyer makes purchase
  result = await marketplace.purchaseProduct(productCount, { from: buyer, value: web3.utils.toWei('1', 'Ether')})

  // Check logs
  const event = result.logs[0].args
  assert.equal(event.id.toNumber(), productCount.toNumber(), 'id is correct')
  assert.equal(event.name, 'iPhone X', 'name is correct')
  assert.equal(event.price, '1000000000000000000', 'price is correct')
  assert.equal(event.owner, buyer, 'owner is correct')
  assert.equal(event.purchased, true, 'purchased is correct')

  // Check that seller received funds
  let newSellerBalance
  newSellerBalance = await web3.eth.getBalance(seller)
  newSellerBalance = new web3.utils.BN(newSellerBalance)

  let price
  price = web3.utils.toWei('1', 'Ether')
  price = new web3.utils.BN(price)

  const exepectedBalance = oldSellerBalance.add(price)

  assert.equal(newSellerBalance.toString(), exepectedBalance.toString())

  // FAILURE: Tries to buy a product that does not exist, i.e., product must have valid id
  await marketplace.purchaseProduct(99, { from: buyer, value: web3.utils.toWei('1', 'Ether')}).should.be.rejected;      // FAILURE: Buyer tries to buy without enough ether
  // FAILURE: Buyer tries to buy without enough ether
  await marketplace.purchaseProduct(productCount, { from: buyer, value: web3.utils.toWei('0.5', 'Ether') }).should.be.rejected;
  // FAILURE: Deployer tries to buy the product, i.e., product can't be purchased twice
  await marketplace.purchaseProduct(productCount, { from: deployer, value: web3.utils.toWei('1', 'Ether') }).should.be.rejected;
  // FAILURE: Buyer tries to buy again, i.e., buyer can't be the seller
  await marketplace.purchaseProduct(productCount, { from: buyer, value: web3.utils.toWei('1', 'Ether') }).should.be.rejected;
})

这个测试的工作方式很像sellProduct()功能测试。它做了几件关键的事情:

  • 检查产品是否已转让给买方
  • 检查卖家是否自动收到加密货币资金
  • 检查所有的失败案例,以确保我们在函数内部防止它们

现在,让我们从命令行运行测试,如下所示:

$ truffle test

🎉耶,他们通过了!

最后,让我们将智能合同部署到网络中,这样我们就可以在下一节开始构建客户端应用程序来与它交互。我们可以使用--reset标志重新运行我们的迁移,将智能合同迁移到网络中。它将在区块链号上创建智能合同的新副本:

$ truffle migrate --reset

干得好!如果你卡住了,请随意再看一遍这部分视频,以便进一步澄清。你也可以在这里找到所有代码的副本

第 4 部分:市场网站设置(前端)

本教程这一部分的视频片段从 01:26:20 开始。你也可以在这里找到的代码

现在让我们开始为市场构建客户端应用程序。我们将在这一部分做以下工作:

  • 启动应用程序,并在我们的浏览器中运行初学者工具包
  • 将我们的网络浏览器连接到区块链
  • 将我们的 web 应用程序连接到区块链,并开始与市场智能合约对话

  • 在我们开始之前,请确保您的开发服务器正在运行:

$ npm run start

这将启动您的 web 服务器,并自动在您的浏览器中打开网站,如下所示:

Blockchain App Template

这个初学者工具包提供了构建我们的应用程序所需的一切:

  • React.js 来构建接口
  • 无需编写 CSS 即可创建 UI 元素的引导程序
  • Web3.js 将我们的应用程序连接到区块链

现在让我们将网络浏览器连接到区块链。我们将为此使用元掩码,它安装在本教程的依赖部分。为此,我们需要做两件事:

  • 将元掩码连接到我们的 Ganache 个人区块链实例
  • 将一些帐户从 Ganache 导入 Metamask,这样我们就可以代表他们作为我们的市场应用程序的用户

你可以在视频中看到我在此时设置元掩码

接下来,让我简单解释一下为什么我们使用 React.js 来构建我们的项目。我们需要一种方法来管理应用程序的所有客户端行为,并存储来自区块链的数据。React.js 不是手动完成所有这些工作,而是通过将代码组织成可重用的组件,并使用状态对象管理应用程序状态,为我们提供了开箱即用的能力。你可以在视频中看到我对 React.js 的完整解释。

现在,让我们借助 a href = " https://web 3js . readthe docs . io/en/1.0/" target = " _ blank " > web 3 . js,将客户端应用程序连接到区块链。我们首先将web3导入到主App.js组件中,如下所示:

import Web3 from 'web3'

现在让我们创建一个新的函数,每当我们的 React 组件被加载时,这个函数都会被调用。在这里,我们将调用一个实例化 web3 的函数。

async componentWillMount() {
  await this.loadWeb3()
}

现在,让我们创建loadWeb3()函数,它将像这样创建连接:

async loadWeb3() {
  if (window.ethereum) {
    window.web3 = new Web3(window.ethereum)
    await window.ethereum.enable()
  }
  else if (window.web3) {
    window.web3 = new Web3(window.web3.currentProvider)
  }
  else {
    window.alert('Non-Ethereum browser detected. You should consider trying MetaMask!')
  }
}

该功能检测网络浏览器中以太坊提供商的存在,允许我们将应用程序连接到区块链。如果您不理解这个函数内部的所有内容,也不用担心。没关系!它直接取自元掩码提供给的指令。:)

现在,让我们创建一个从区块链加载数据的函数。我们先这样称呼它:

async componentWillMount() {
  await this.loadWeb3()
  await this.loadBlockchainData()
}

我们将这样定义它:

async loadBlockchainData() {
  const web3 = window.web3
}

在这里,我们将存储的 web3 连接连接到一个变量。现在,让我们从元掩码中获取帐户,并将它们记录到控制台,如下所示:

async loadBlockchainData() {
  const web3 = window.web3
  const accounts = await web3.eth.getAccounts()
  console.log(accounts)
}

现在转到浏览器,查看登录到控制台的帐户!

接下来,我们将把这个帐户存储到 React 状态对象中,以便以后使用:

async loadBlockchainData() {
  const web3 = window.web3
  const accounts = await web3.eth.getAccounts()
  console.log(accounts)
  this.setState({ account: accounts[0] })
}

现在,让我们为 state 对象设置一些默认值。React 让我们这样做:

constructor(props) {
  super(props)
  this.state = {
    account: '',
    productCount: 0,
    products: [],
    loading: true
  }
}

现在,我们可以在页面的导航栏中列出帐户。在这里,我将继续为导航栏创建一个新组件。我在视频的最后做这个,所以有点乱。然而,我认为这是做这件事的最佳时机。

首先,在与App.js相同的目录中创建一个名为Navbar.js的新组件。然后在该文件中使用以下代码:

import React, { Component } from 'react';

class Navbar extends Component {

  render() {
    return (
      <nav className="navbar navbar-dark fixed-top bg-dark flex-md-nowrap p-0 shadow">
        <a
          className="navbar-brand col-sm-3 col-md-2 mr-0"
          href="http://www.dappuniversity.com/bootcamp"
          target="_blank"
          rel="noopener noreferrer"
        >
          Dapp University's Blockchain Marketplace
        </a>
        <ul className="navbar-nav px-3">
          <li className="nav-item text-nowrap d-none d-sm-none d-sm-block">
            <small className="text-white"><span id="account">{this.props.account}</span></small>
          </li>
        </ul>
      </nav>
    );
  }
}
export default Navbar;

注意,它用{this.props.account}读取账户。这使用了 React 的 props 对象,该对象可用于所有 React 组件。当我们在App.js中渲染这个组件时,我们需要将这些道具传递给它。让我们接下来做那件事。

现在导入App.js顶部的导航条组件:

import Navbar from './Navbar'

现在让我们把它呈现在页面上。首先,删除所有旧的 Navbar 代码,并替换为:

<Navbar account={this.state.account} />

注意,我们从 React 的状态对象中读取帐户,然后通过 props 将其传递给 Navbar 组件。现在,在网络浏览器中访问您的应用程序,查看页面上列出的帐户!

接下来,让我们将连接添加到市场智能合约。首先,我们将在App.js的顶部导入智能合约 ABI,如下所示:

import Marketplace from '../abis/Marketplace.json'

现在,让我们更新loadBlockchainData()函数来连接智能合约:

async loadBlockchainData() {
  const web3 = window.web3
  // Load account
  const accounts = await web3.eth.getAccounts()
  this.setState({ account: accounts[0] })
  const networkId = await web3.eth.net.getId()
  const networkData = Marketplace.networks[networkId]
  if(networkData) {
    const marketplace = web3.eth.Contract(Marketplace.abi, networkData.address)
    console.log(marketplace)
  } else {
    window.alert('Marketplace contract not deployed to detected network.')
  }
}

这段代码做了几件事:

  • 读取 networkID 以确定我们使用 Metamask 连接到哪个网络,即 Ganache。例如,我们将使用这个网络 ID 连接到部署到 Ganache 网络的智能合约,而不是主以太坊网络。
  • 接下来,我们用web3.eth.Contract()实例化 Web3.js 的智能合约。为此,我们需要两条信息:智能合同 ABI 和地址。我们从刚刚导入的文件中获取这两者。
  • 最后,如果我们在网络上找不到智能合约,我们会提醒用户。您可以通过切换到 Metamask 中的主以太坊网络来测试这一点(只是不要忘记切换回来)。

现在,您的最终组件应该如下所示:

import React, { Component } from 'react';
import Web3 from 'web3'
import logo from '../logo.png';
import './App.css';
import Marketplace from '../abis/Marketplace.json'
import Navbar from './Navbar'

class App extends Component {

  async componentWillMount() {
    await this.loadWeb3()
    await this.loadBlockchainData()
  }

  async loadWeb3() {
    if (window.ethereum) {
      window.web3 = new Web3(window.ethereum)
      await window.ethereum.enable()
    }
    else if (window.web3) {
      window.web3 = new Web3(window.web3.currentProvider)
    }
    else {
      window.alert('Non-Ethereum browser detected. You should consider trying MetaMask!')
    }
  }

  async loadBlockchainData() {
    const web3 = window.web3
    // Load account
    const accounts = await web3.eth.getAccounts()
    this.setState({ account: accounts[0] })
    const networkId = await web3.eth.net.getId()
    const networkData = Marketplace.networks[networkId]
    if(networkData) {
      const marketplace = web3.eth.Contract(Marketplace.abi, networkData.address)
      console.log(marketplace)
    } else {
      window.alert('Marketplace contract not deployed to detected network.')
    }
  }

  constructor(props) {
    super(props)
    this.state = {
      account: '',
      productCount: 0,
      products: [],
      loading: true
    }
  }

  render() {
    return (
      <div>
        <Navbar account={this.state.account} />
        <div className="container-fluid mt-5">
          <div className="row">
            <main role="main" className="col-lg-12 d-flex text-center">
              <div className="content mr-auto ml-auto">
                <a
                  href="http://www.dappuniversity.com/bootcamp"
                  target="_blank"
                  rel="noopener noreferrer"
                >
                  <img src={logo} className="App-logo" alt="logo" />
                </a>
                <h1>Dapp University Starter Kit</h1>
                <p>
                  Edit <code>src/components/App.js</code> and save to reload.
                </p>
                <a
                  className="App-link"
                  href="http://www.dappuniversity.com/bootcamp"
                  target="_blank"
                  rel="noopener noreferrer"
                >
                  LEARN BLOCKCHAIN <u><b>NOW! </b></u>
                </a>
              </div>
            </main>
          </div>
        </div>
      </div>
    );
  }
}

export default App;

哇!在这一部分,我们很快就谈到了很多内容。如果信息很多也不用担心。请随意再看一遍这部分视频,以便进一步澄清。你也可以在这里找到所有代码的副本

第 5 部分:销售产品(前端)

本教程这一部分的视频片段从 01:53:10 开始。你也可以在这里找到的代码

现在让我们创建一种从我们的市场网站销售产品的方法。我们将完成以下任务:

  • 创建一个 react 组件来保存我们代码的框架,包括一个允许用户列出新产品的表单和一个显示待售产品的表格。
  • 我们将连接表单,以便用户可以在区块链上实际列出他们的待售产品。

首先,让我们向我们的loadBlockchainData()函数添加一些数据。我们会得到productCount

async loadBlockchainData() {
  const web3 = window.web3
  // Load account
  const accounts = await web3.eth.getAccounts()
  this.setState({ account: accounts[0] })
  const networkId = await web3.eth.net.getId()
  const networkData = Marketplace.networks[networkId]
  if(networkData) {
    const marketplace = web3.eth.Contract(Marketplace.abi, networkData.address)
    this.setState({ marketplace })
    const productCount = await marketplace.methods.productCount().call()
    console.log(productCount.toString())
    this.setState({ loading: false})
  } else {
    window.alert('Marketplace contract not deployed to detected network.')
  }
}

接下来,我们将创建一个函数,通过用 Web3.js 调用createProduct()函数将产品添加到区块链,如下所示:

createProduct(name, price) {
this.setState({ loading: true })
this.state.marketplace.methods.createProduct(name, price).send({ from: this.state.account })
.once('receipt', (receipt) => {
  this.setState({ loading: false })
})

让我解释一下这段代码:

  • 首先,我们创建一个 JavaScript 函数,它接受与智能契约函数相同的参数
  • 然后在调用智能合约函数之前,我们告诉 React 我们的应用程序正在“加载”,以便用户知道函数已提交。
  • 然后我们用 Web3.js 用this.state.marketplace.methods.createProduct(name, price).send({ from: this.state.account })调用智能合约函数。这将调用该函数,并告诉 Web3 当前帐户是调用它的用户。
  • 最后,一旦收到交易收据,我们将应用程序从“加载”状态中移除,这样用户就知道函数调用完成了。

为了用表单调用这个函数,我们必须将它绑定到构造函数内部的组件,如下所示:

constructor(props) {
  // ...
  this.createProduct = this.createProduct.bind(this)
}

现在我们可以把它传递给子组件。但首先我们必须创造它!让我们在当前目录下创建一个名为

Main.js

with the following code:

import React, { Component } from 'react';

class Main extends Component {

  render() {
    return (
      <div id="content">
        <h1>Add Product</h1>
        <form onSubmit={(event) => {
          event.preventDefault()
          const name = this.productName.value
          const price = window.web3.utils.toWei(this.productPrice.value.toString(), 'Ether')
          this.props.createProduct(name, price)
        }}>
          <div className="form-group mr-sm-2">
            <input
              id="productName"
              type="text"
              ref={(input) => { this.productName = input }}
              className="form-control"
              placeholder="Product Name"
              required />
          </div>
          <div className="form-group mr-sm-2">
            <input
              id="productPrice"
              type="text"
              ref={(input) => { this.productPrice = input }}
              className="form-control"
              placeholder="Product Price"
              required />
          </div>
          <button type="submit" className="btn btn-primary">Add Product</button>
        </form>
        <p> </p>
        <h2>Buy Product</h2>
        <table className="table">
          <thead>
            <tr>
              <th scope="col">#</th>
              <th scope="col">Name</th>
              <th scope="col">Price</th>
              <th scope="col">Owner</th>
              <th scope="col"></th>
            </tr>
          </thead>
          <tbody id="productList">
            <tr>
              <th scope="row">1</th>
              <td>iPhone x</td>
              <td>1 Eth</td>
              <td>0x39C7BC5496f4eaaa1fF75d88E079C22f0519E7b9</td>
              <td><button className="buyButton">Buy</button></td>
            </tr>
            <tr>
              <th scope="row">2</th>
              <td>Macbook Pro</td>
              <td>3 eth</td>
              <td>0x39C7BC5496f4eaaa1fF75d88E079C22f0519E7b9</td>
              <td><button className="buyButton">Buy</button></td>
            </tr>
            <tr>
              <th scope="row">3</th>
              <td>Airpods</td>
              <td>0.5 eth</td>
              <td>0x39C7BC5496f4eaaa1fF75d88E079C22f0519E7b9</td>
              <td><button className="buyButton">Buy</button></td>
            </tr>
          </tbody>
        </table>
      </div>
    );
  }
}

export default Main;

该组件为市场 UI 添加了所有的脚手架。它还创建了一个表单,通过调用createProduct()函数将产品添加到区块链。请随意在视频中查看这段代码的完整解释。

但是这个组件现在还不能工作!我们需要先做几件事。返回到App.js,在文件顶部导入新创建的组件,如下所示:

import Main from './Main'

现在用下面的代码替换<main> HTML 标签中的所有内容:

<main role="main" className="col-lg-12 d-flex">
  { this.state.loading
    ? <div id="loader" className="text-center"><p className="text-center">Loading...</p></div>
    : <Main createProduct={this.createProduct} />
  }
</main>

每当加载应用程序时,这将在页面上呈现新创建的组件。如果是加载,那么就会显示加载器!

🎉耶!现在,进入您的浏览器,尝试使用这种新形式添加产品!

哇!在这一部分,我们很快就谈到了很多内容。如果信息很多也不用担心。请随意再看一遍这部分视频,以便进一步澄清。你也可以在这里找到所有代码的副本

第 6 部分:购买产品(前端)

本教程这一部分的视频片段从 02:11:53 开始。你也可以在这里找到的代码

现在让我们通过允许用户在市场中购买产品来结束我们的应用程序。在这一部分,我们将做两件事:

  • 在页面上列出所有产品
  • 允许用户通过点击按钮来购买它们

首先,让我们像这样从loadBlockchainData()函数内的区块链获取所有产品:

const productCount = await marketplace.methods.productCount().call()
    this.setState({ productCount })
    // Load products
    for (var i = 1; i <= productCount; i++) {
      const product = await marketplace.methods.products(i).call()
      this.setState({
        products: [...this.state.products, product]
      })
    } 

我们使用在智能合约中创建的计数器缓存来确定存在多少产品,然后使用 for 循环来单独获取每个产品,并将其存储到 react 状态对象中。有了这些信息,我们可以立即在页面上呈现产品。

现在,让我们创建一个购买产品的函数:

purchaseProduct(id, price) {
  this.setState({ loading: true })
  this.state.marketplace.methods.purchaseProduct(id).send({ from: this.state.account, value: price })
  .once('receipt', (receipt) => {
    this.setState({ loading: false })
  })
}

现在让我们将函数绑定到构造函数中:

constructor(props) {
  // ...
  this.createProduct = this.createProduct.bind(this)
  this.purchaseProduct = this.purchaseProduct.bind(this)
}

现在我们将把两个新道具传给Main component:

  • 页面上可以列出的所有产品
  • purchaseProdct()函数,这样它可以在子组件中被调用

我们将更新代码,使其看起来像这样:

<main role="main" className="col-lg-12 d-flex">
  { this.state.loading
    ? <div id="loader" className="text-center"><p className="text-center">Loading...</p></div>
    : <Main
      products={this.state.products}
      createProduct={this.createProduct}
      purchaseProduct={this.purchaseProduct} />
  }
</main>

厉害!现在让我们更新Main.js文件中的代码,列出所有产品,并允许用户购买。用以下代码替换<tbody>标签中的所有代码:

{ this.props.products.map((product, key) => {
  return(
    <tr key={key}>
      <th scope="row">{product.id.toString()}</th>
      <td>{product.name}</td>
      <td>{window.web3.utils.fromWei(product.price.toString(), 'Ether')} Eth</td>
      <td>{product.owner}</td>
      <td>
        { !product.purchased
          ? <button
              name={product.id}
              value={product.price}
              onClick={(event) => {
                this.props.purchaseProduct(event.target.name, event.target.value)
              }}
            >
              Buy
            </button>
          : null
        }
        </td>
    </tr>
  )
})}

这段代码做了几件事:

  • 遍历所有产品,并为每个产品创建唯一的表行
  • 添加一个按钮,这样只需单击一下就可以购买产品,调用我们刚刚创建的purchaseProduct()函数。

厉害!现在转到您的浏览器并尝试购买产品!

FWEW!在这一部分,我们确实很快地介绍了很多信息...如果你第一次没有完全理解,也不要担心。请随意再看一遍这部分视频,以便进一步澄清。你也可以在这里找到所有代码的副本

第 7 部分:部署项目

这部分教程的视频片段从 02:27:44 开始。你也可以在这里找到的代码

现在让我们将我们的项目部署到一个真实的区块链。如果您能在视频中跟随我,而不是写出这一部分的每个步骤,效果会更好。我专门为这次部署添加了一个单独的视频,您可以在这里观看:

https://www.youtube.com/embed/TMnjVgZO7B4

万岁!🎉您已经构建了一个全栈区块链应用程序!恭喜你,你已经在本教程中走了这么远。

对这个教程满意吗?然后你需要在这里参加我的免费培训,我将向你展示如何构建一个真实世界的区块链应用程序,这样你就可以成为一名高薪的区块链开发者!

*


我们一直在努力

apachecn/AiLearning

【布客】中文翻译组