跳转至

创建基于区块链的电子商务市场

一个分散的电子商务市场是区块链技术的最佳案例之一,原因很简单,你不必支付费用,也不必将你的数据委托给有实力的公司,这些公司会出售这些数据以获取利润。以太坊是一个很好的解决方案,新的 ERC-721 令牌标准已经被批准,你可以在区块链上生成数字化的对象。在这一章中,你将学习如何处理个人用户数据,从而保护每个人的数据,因为以太坊是一个公共系统。

在第一部分,我们将看看一个电子商务网站应该如何构建,以便用户可以与它互动,就像它是一个真正的商店。您将构建一个用户界面,显示符合 ERC-721 协议的独特产品。然后,您将实现 React 路由器模块,在一个用户友好的界面中组织您的不同视图。最后,您将创建实现 ERC-721 令牌的智能契约,并创建管理分散产品所需的函数。

此外,在本章中,您将通过学习以下主题,了解如何在以太坊上为您的企业创建功能齐全的电子商务市场:

  • 创建用户界面
  • 了解 ERC-721 令牌
  • 开发电子商务智能合同
  • 完成 dApp

创建用户界面

这些类型的指南的伟大之处在于,你可以在这里学到关于分散电子商务的知识,并扩展这些想法,以创建一个更先进的产品,提供一个复杂的解决方案来筹集资金,或者你可以简单地建立一个业务。

规划市场界面

这个市场有几乎无限的选择,因为你不必面对许多区块链的限制。每个产品都是一个独立的实例,可以根据您的需要进行塑造,因此您可以根据需要添加任意多的功能,例如:

  • 购物时将产品添加到购物车中的运输系统,而不是直接购买更大的组合商品
  • 一个动态送货地址功能,添加几个不同的地址,以便您可以通过保存您的首选位置将订单快速发送到许多位置
  • 为用户产品创建拍卖的竞价系统
  • 描述和检查功能以实现更好的用户交互

在这个项目中,我们不会实现任何这些高级功能,因为它们需要太多的时间来开发,尽管你可以在基本产品完成后自己添加它们。因此,我们将创建一个具有以下功能的简单界面:

  • 通过以太坊直接购买实物和数字产品的购买系统
  • 作为独立销售者向市场发布产品的销售功能
  • 订单显示功能,以买方和卖方的身份查看未决订单

总的来说,用户将能够像普通的在线商店一样直接付款,使用 MetaMask 而不是信用卡。该市场不会向用户收取费用,相比之下,亚马逊等电子商务商店收取的费用约占总支付额的 15%,这实际上是加起来的。另一个要点是,将不会有任何审查或规则可循,这意味着用户可以自由发布产品,而不必担心中央实体的禁令,这是一个反复出现的问题,影响了卖家,导致他们在锁定资金和恢复订单方面损失数千美元。

一种产品不会有几种数量,因为我们将使用独特的、不可替代的代币 ( NFTs ),这意味着每种产品都必须是独特的。由于我们将从一个用户到另一个用户交换令牌,我们将无法拥有同一产品的多个副本。但是,您可以实现一个 ERC-20 令牌或一个系统来为多个数量的相同产品生成相同令牌 ID 的多个副本。

让我们从通过克隆基本存储库(https://github.com/merlox/dapp)或者通过自己配置npm和 Truffle 来设置项目开始。设置 Truffle 或克隆存储库后,您应该有以下文件夹和初始文件:

  • contracts/
  • dist/
  • migrations/
  • node_modules/(记住在克隆存储库后使用npm install)
  • src/

    • index.js
    • 根据您的喜好选择index.htmlindex.ejs
    • 根据您的喜好选择index.cssindex.styl
  • .babelrc

  • .gitignore
  • LICENSE
  • package.json
  • README.md
  • truffle-config.js
  • webpack.config.js(记得设置您的 webpack 配置)

在您的src/文件夹中,创建一个名为components/的新文件夹,其中将包含每个 JavaScript 组件的文件,因为这是一个更大的 dApp,我们将有许多不同的组件。因为我们将有多个页面,所以我们希望使用 react 路由器来管理历史位置和 URL,以便用户能够在页面之间导航。通过在终端上运行以下命令来安装 React 路由器和web3库:

npm i -S web3 react-router-dom

设置索引页面

打开你的index.js文件,导入需要的库,用一些占位符产品用假数据设置初始状态,只是为了看看最终设计会是什么样子。我们通过以下步骤来实现这一点:

  1. 导入所需的库。我们需要来自react-router库中的几个组件,如下面的代码所示:
import React from 'react'
import ReactDOM from 'react-dom'
import MyWeb3 from 'web3'
import { BrowserRouter, Route, withRouter } from 'react-router-dom'
  1. 使用一些产品创建构造函数,这些产品具有向用户显示尽可能多的信息的必要属性,如下面的代码所示。诸如titledescriptionidprice的属性是必须的:
class Main extends React.Component {
    constructor(props) {
        super(props)

        this.state = {
            products: [{
                id: 1,
                title: 'Clasic trendy shoes',
                description: 'New unique shoes for sale',
                date: Date.now(),
                owner: '',
                price: 12,
                image: 'https://cdn.shopify.com/s/files/1/2494/8702/products/Bjakin-2018-Socks-Running-Shoes-for-Men-Lightweight-Sports-Sneakers-Colors-Man-Sock-Walking-Shoes-Big_17fa0d5b-d9d9-46a0-bdea-ac2dc17474ce_400x.jpg?v=1537755930'
            }
            productsHtml: [],
            productDetails: [],
            product: {},
        }
    }
  1. 您可以通过复制product对象并更改一些参数来添加更多产品,使其看起来独一无二。然后添加将字符串转换成有效十六进制的bytes32()函数和render()函数,如下面的代码所示:
    bytes32(name) {
        return myWeb3.utils.fromAscii(name)
    }

    render() {
        return (
            <div>
                <Route path="/" exact render={() => (
                    <div>The dApp has been setup</div>
                )} /> 
            </div>
        )
    }
}
  1. 使用 React 路由器提供的withRouter()函数为我们的Main组件提供 history 属性,这是在 dApp 中的页面之间导航所必需的。这显示在以下代码中:
// To be able to access the history in order to redirect users programmatically when opening a product
Main = withRouter(Main)
  1. 从 react 路由器添加BrowserRouter组件来初始化路由器对象,如以下代码所示:
ReactDOM.render(
    <BrowserRouter>
        <Main />
    </BrowserRouter>,
document.querySelector('#root'))

BrowserRouter组件是用于初始化路由器的主要组件,以便它们可以管理不同的页面。我们使用withRouter导入来访问导航历史,这样我们就可以通过编程来改变页面。基本上,我们需要它在我们需要的特定时间将用户重定向到我们 dApp 中的不同页面。然后我们在this.state对象中设置一些具有不同属性的基本产品。注意图像是一个 URL 而不是一个文件。由于我们没有处理文件的服务器,我们需要卖家在某种公共服务上托管自己的图片,比如 Imgur。

React 路由器库将使用几个Route实例来决定在什么时间加载哪个页面。我们还必须在我们的Main组件之上添加高级的BrowserRouter组件来激活路由器。请注意我们是如何使用exact path="/"呈现一条路线的,它显示了设置文本,以确认应用程序在配置后已成功加载。

配置 webpack 开发服务器

在创建了Main组件之后,您会想要运行应用程序来看看它看起来如何,然而,在这种情况下,我们将使用webpack-dev-server扩展,它会在我们开发时自动重新加载网站,这样我们就不必经常手动重新加载它并在后端编译文件。因此,不用设置 webpack 监视器和静态服务器,所有这些都包含在一个命令中。使用以下命令在本地安装 webpack 服务器:

npm i -S webpack-dev-server

然后在scripts部分下用一个新脚本更新您的package.json文件(如下面的代码所示);否则,它将不起作用,因为我们需要从项目内部执行这个命令:

{
  "name": "dapp",
  "version": "1.0.0",
  "description": "",
  "main": "truffle-config.js",
  "directories": {
    "test": "test"
  },
 "scripts": {
 "dev": "webpack-dev-server -d"
 }
}

这只是运行带有-d标志的webpack-dev-server命令,将模式设置为开发,允许您查看来自未压缩文件的完整错误消息。如果您愿意,您可以添加-o标志,当您运行该命令时,它会打开一个浏览器。通过运行以下命令行来执行它:

npm run dev

如果一切都正确,您将能够转到localhost:8080并看到您的设置了路由器的页面。

创建标题组件

我们的应用程序将有几个买家,卖家和订单页面。这就是为什么尽可能将每个组件分成唯一的块非常重要,这些块可以通过执行以下步骤导入到需要的地方:

  1. src/components/文件夹中创建一个新组件来显示我们网站的标题,并在您的components文件夹中创建一个名为Header.js的文件,如下面的代码所示:
import React from 'react'
import { Link } from 'react-router-dom'

function Header() {
    return (
        <div className="header">
            <Link to="/">ECOMMERCE</Link>
            <div>
                <Link to="/">Home</Link>
                <Link to="/sell">Sell</Link>
                <Link to="/orders">Orders</Link>
            </div>
        </div>
    )
}

export default Header
  1. export default Header导出它,以便其他文件可以访问你的组件。然后将它导入到您的index.js页面,如下面的代码所示,显示在您导入的库的正下方,以保持它们的有序:
import React from 'react'
import ReactDOM from 'react-dom'
import MyWeb3 from 'web3'
import { BrowserRouter, Route, withRouter } from 'react-router-dom'
import Header from './components/Header'
  1. 用组件实例更新您的render()函数,如下面的代码所示:
render() {
    return (
        <div>
            <Route path="/" exact render={() => (
                <Header />
            )} />
        </div>
    )
}

您将看到无需刷新 webpack 服务即可自动加载您的标题,如以下屏幕截图所示:

  1. 现在还不好看,还是用一些stylus CSS 改进一下设计吧。如果您还没有配置它,用下面的命令安装stylusstylus-loader库:
npm i -S stylus stylus-loader
  1. 按如下方式更新您的webpack配置:
require('babel-polyfill')
const webpack = require('webpack')
const html = require('html-webpack-plugin')
const path = require('path')

module.exports = {
    entry: ['babel-polyfill', './src/index.js'],
    output: {
        filename: 'bundle.js',
        path: path.join(__dirname, 'dist')
    },
    module: {
        rules: [
            {
                test: /\.js$/,
                exclude: /node_modules/,
                use: {
                    loader: 'babel-loader'
                }
            }, {
 test: /\.styl$/,
 exclude: /node_modules/,
 use: [
 {loader: 'style-loader'},
 {loader: 'css-loader'},
 {loader: 'stylus-loader'}
 ]
 }
        ]
    },
    plugins: [
        new webpack.HotModuleReplacementPlugin(),
        new html({
            title: "dApp project",
            template: './src/index.ejs',
            hash: true
        })
    ]
}

以下是我们在webpack文件中更改的主要内容:

  • 我们导入了 webpack,这样当我们进行更改时,可以使用webpack.HotModuleReplacementPlugin()来重新加载页面的一部分。将只重新加载已更改的组件,而不是重新加载整个页面。
  • 然后我们设置手写笔加载器来加载styl文件。

  • 使用以下设计创建index.styl,尽管你的电子商务商店的最终外观取决于你自己:

productPadding = 20px

body
    background-color: whitesmoke
    font-family: sans-serif
    margin: 0

button
    border: none
    background-color: black
    color: white
    cursor: pointer
    padding: 10px
    width: 200px
    height: 50px

    &:hover
        opacity: 0.9

input, textarea
    padding: 20px
    border: 1px solid black

.header
    background-color: black
    color: white
    padding: 15px
    margin-bottom: 20px
    text-align: center
    display: flex
    justify-content: space-around

    a
        color: white
        text-decoration: none
        margin-right: 10px

        &:hover
            color: lightgrey
  1. 注意顶部的productPadding变量。Stylus 允许我们创建变量,这样我们就可以轻松地跨样式文件配置相同值的多个实例;我们稍后会用到这个变量。然后像这样将手写笔文件导入到您的index.js文件中:
import './index.styl'

现在检查你的应用在浏览器中的外观;由于更新了 webpack 配置,您可能需要重新加载 webpack 服务器:

创建主构件

组件将包含显示用户第一次打开 dApp 时看到的第一页的逻辑,以便他们可以开始购买产品。该组件将是管理其余页面的核心组件。

使用主页的默认设计创建一个Home组件;它将包含设计简洁的最新产品。下面是 components 文件夹中的Home.js文件的代码:

import React from 'react'
import MyWeb3 from 'web3'
import Header from './Header'

class Home extends React.Component {
    constructor() { super() }
    render() {
        return (
            <div>
                <Header />
                <div className="products-container">{this.props.productsHtml}</div>
                <div className="spacer"></div>
            </div>
        )
    }
}

export default Home

您可以将其导入到您的index.js文件中,这将是数据和函数的主要来源。还要删除索引中的Header导入,因为它已经包含在Home组件中。以下步骤显示了将Home组件包含在 dApp 中必须进行的更改:

  1. 在删除Header组件的同时导入文件开头的组件,因为我们已经将它包含在Home组件中:
import React from 'react'
import ReactDOM from 'react-dom'
import MyWeb3 from 'web3'
import { BrowserRouter, Route, withRouter } from 'react-router-dom'
import Home from './components/Home'
import './index.styl'
  1. 为了简单起见,我为Array对象创建了一个原型 JavaScript 方法。这是 JavaScript 方法的一个高级实现,可以用来改变某些函数的工作方式。特别是,我创建了一个异步的for循环,它可以是awaited,以确保它在继续其余代码之前完成,如下面的代码片段所示。本质上,这是一种运行循环的干净方式:
Array.prototype.asyncForEach = function (callback) {
 return new Promise(resolve => {
 for(let i = 0; i < this.length; i++) {
 callback(this[i], i, this)
 }
 resolve()
 })
}
  1. 在您的构造函数中,包含一个setup()函数调用,如下面的代码片段所示:
constructor(props) {
    super(props)
    // State object omitted for simplicity
    this.setup()
}
  1. 用代码实现setup()函数来启动 web3 实例并显示产品,如下面的代码片段所示:
async setup() {
    // Create the contract instance
    window.myWeb3 = new MyWeb3(ethereum)
    try {
        await ethereum.enable();
    } catch (error) {
        console.error('You must approve this dApp to interact with it')
    }
    const user = (await myWeb3.eth.getAccounts())[0]
    let products = []
    for(let i = 0; i < this.state.products.length; i++) {
        products[i] = this.state.products[i]
        products[i].owner = user
    }
    this.setState({products})
    this.displayProducts()
}
  1. 我们已经包含了对displayProducts()函数的调用,该函数将通过在state对象中循环我们的产品数组来显示产品,如下面的代码片段所示:
async displayProducts() {
    let productsHtml = []
    await this.state.products.asyncForEach(product => {
        productsHtml.push((
            <div key={product.id} className="product">
                <img className="product-image" src={product.image} />
                <div className="product-data">
                    <h3 className="product-title">{product.title}</h3>
                    <div className="product-description">{product.description.substring(0, 50) + '...'}</div>
                    <div className="product-price">{product.price} ETH</div>
                    <button onClick={() => {
                        this.setState({product})
                        this.redirectTo('/product')
                    }} className="product-view" type="button">View</button>
                </div>
            </div>
        ))
    })
    this.setState({productsHtml})
}
  1. 修改render()函数并包含一个名为redirectTo()的函数,当用户使用 React 路由器点击一个按钮时,它将允许您改变页面,如下面的代码片段所示:
    redirectTo(location) {
 this.props.history.push({
 pathname: location
 })
 }

    render() {
        return (
            <div>
                <Route path="/" exact render={() => (
 <Home
 productsHtml={this.state.productsHtml}
 />
                )} />
            </div>
        )
    }
}

我们对此索引文件做了以下重要的补充:

  • 首先,我们为Array对象设置了一个定制的原型函数,命名为asyncForEach。您可能不太熟悉 JavaScript 的深层工作原理,但是您必须理解所有类型的变量都是具有名为prototype的属性的对象,该属性包含该类型变量的方法。默认的forEach方法在 JavaScript 中被定义为Array.prototype.forEach = function () {...};这样做的目的是创建一个自定义的for外观,我们可以await直到它完成,以充分利用async功能。因此,我们可以键入await array.asyncForEach(),而不是键入for(let i = 0; i < array.length; i++) {},这样更容易阅读,代码也更容易混淆。这只是一个实现,我想用它来提高代码的可读性,同时增加其可用性。

  • 然后我们导入了Home组件而不是Header组件,并将其替换在Route内的render()函数中。

  • redirectTo函数通过使用我们之前看到的withRouter历史对象加载一个新页面来改变我们当前看到的Route。当用户点击displayProducts功能内的View按钮时,将使用该功能。
  • 在这之后,我们添加了一个setup函数来配置元掩码,同时将所有者地址添加到这些示例产品中,这样您就可以看到谁拥有这些对象。
  • 最后,我们创建了一个displayProducts()函数,它为每个产品生成 HTML,同时将它推入产品数组并更新状态。然后,Home组件以prop的形式接收这些产品,并显示每个产品。

现在我们可以添加一些 CSS 代码来改善主页的外观,如下所示:

.products-container
    display: grid
    width: 80%
    margin: auto
    grid-template-columns: 1fr 1fr 1fr
    justify-items: center
    margin-top: 50px

    .product
        width: 400px
        border: 1px solid black

        .product-image
            width: 100%
            grid-column: 1 / 3
            box-shadow: 0 3px 0px 0 lightgrey

        .product-data
            display: grid
            grid-template-columns: 1fr 1fr
            grid-template-rows: 50px 20px 40px
            align-items: center
            padding: 10px productPadding
            grid-column-gap: productPadding
            background-color: white

            .product-description
                font-size: 10pt

            .product-price
                font-size: 11pt

            .product-view
                width: 200px
                grid-column: 2 / 3
                margin-top: 50px
                height: 50px

.spacer
    height: 200px
    width: 100%

现在,网页如下所示:

如你所见,我们进展很快!对于这种复杂的应用程序,初始设置需要花费一些时间,但这是一件非常好的事情,因为您可以轻松地更新每个单独的部分,同时保证将来改进的可维护性。这家电子商务商店的主题与许多鞋店相似:它使用扁平的设计和黑色色调,同时也加入按钮等元素,给它带来立体感。这让我想起了一本时尚杂志。

创建产品组件

现在我们有了一个基本的设计,我们可以在用户点击 View 按钮时创建产品页面,这样用户就可以看到关于这个特定产品的更多详细信息。用户将能够在产品页面内购买产品。让我们来完成以下步骤:

  1. 使用以下代码在组件中添加一个新的Product.js文件,尽管我总是建议您在看到解决方案之前亲自尝试一下:
import React from 'react'
import Header from './Header'

class Product extends React.Component {
    constructor() { super() }
    render() {
        return (
            <div>
                <Header />
                <div className="product-details">
                    <img className="product-image" src={this.props.product.image} />
                    <div className="product-data">
                        <h3 className="product-title">{this.props.product.title}</h3>
                        <ul className="product-description">
                            {this.props.product.description.split('\n').map((line, index) => (
                                <li key={index}>{line}</li>
                            ))}
                        </ul>
                        <div className="product-data-container">
                            <div className="product-price">{this.props.product.price} ETH</div>
                            <div className="product-quantity">{this.props.product.quantity} units available</div>
                        </div>
                        <button onClick={() => {
                            this.props.redirectTo('/buy')
                        }} className="product-buy" type="button">Buy</button>
                    </div>
                </div>
            </div>
        )
    }
}

export default Product
  1. 我们需要一个新的 header,因为当我们改变页面时,一个新的组件将被加载(在本例中是Product组件),所以我们只需要向Product组件显示必要的信息。然后我们可以将其导入到一个新的Route索引文件中,如下面的代码所示:
import React from 'react'
import ReactDOM from 'react-dom'
import MyWeb3 from 'web3'
import { BrowserRouter, Route, withRouter } from 'react-router-dom'
import Home from './components/Home'
import Product from './components/Product'
import './index.styl'

class Main extends React.Component {
    // Omitted previous code to keep the demonstration short 

    render() {
        return (
            <div>
                <Route path="/" exact render={() => (
                    <Home
                        productsHtml={this.state.productsHtml}
                    />
                )} />
 <Route path="/product" render={() => (
 <Product
 product={this.state.product}
 />
                )} />
            </div>
        )
    }
}
  1. 假设我们设置了所需的历史功能,那么当您单击“查看”按钮时,您应该能够访问定制产品页面。当用户单击 View 按钮时,还会设置Product组件的product属性。添加以下 CSS 代码以修复产品页面的设计:
.product-details
    display: grid
    width: 70%
    margin: auto
    grid-template-columns: 70% 30%
    grid-template-rows: 1fr
    margin-bottom: 50px
    grid-column-gap: 40px

    .product-image
        grid-column: 1 / 2
        justify-self: center

    .product-title, .product-description, .product-price, .product-buy
        grid-column: 2 / 3

    .product-description
        white-space: pre-wrap
        line-height: 20pt

    .product-data-container
        display: flex
        justify-content: space-between
        margin-bottom: 20px
  1. 您可以打开 dApp,单击产品的“查看”按钮,查看详细的产品页面,其中显示了大图和完整描述,如以下屏幕截图所示:

剩下的就是添加购买、销售和订单页面。下面是我们如何使用Buy组件,当用户点击位于产品页面的Buy按钮时,它将会显示出来:

  1. 使用以下命令导入所需的库:
import React, { Component } from 'react'
import Header from './Header'
  1. 用空的状态变量定义Buy组件中的构造函数,这样你就知道哪些变量将在整个组件中使用,你可以通过使用下面的代码来实现:
class Buy extends Component {
    constructor() {
        super()
        this.state = {
            nameSurname: '',
            lineOneDirection: '',
            lineTwoDirection: '',
            city: '',
            stateRegion: '',
            postalCode: '',
            country: '',
            phone: '',
        }
    }
  1. render页面函数将显示一些基本的产品信息,以告知买家他们将得到什么,如下面的代码所示:
    render() {
        return (
            <div>
                <Header />
                <div className="product-buy-page">
                    <h3 className="title">Product details</h3>
                    <img className="product-image" src={this.props.product.image} />
                    <div className="product-data">
                        <p className="product-title">{this.props.product.title}</p>
                        <div className="product-price">{this.props.product.price} ETH</div>
                    </div>
                </div>
  1. 为用户提供一个包含送货信息的块,以包含他们的地址,这样他们就可以免费收到产品,如下面的代码所示:
                <div className="shipping-buy-page">
                    <h3>Shipping</h3>
                    <input onChange={e => {
                        this.setState({nameSurname: e.target.value})
                    }} placeholder="Name and surname..." type="text" />
                    <input onChange={e => {
                        this.setState({lineOneDirection: e.target.value})
                    }} placeholder="Line 1 direction..." type="text" />
                    <input onChange={e => {
                        this.setState({lineTwoDirection: e.target.value})
                    }} placeholder="Line 2 direction..." type="text" />
                    <input onChange={e => {
                        this.setState({city: e.target.value})
                    }} placeholder="City..." type="text" />
                    <input onChange={e => {
                        this.setState({stateRegion: e.target.value})
                    }} placeholder="State or region..." type="text" />
                    <input onChange={e => {
                        this.setState({postalCode: e.target.value})
                    }} placeholder="Postal code..." type="number" />
                    <input onChange={e => {
                        this.setState({country: e.target.value})
                    }} placeholder="Country..." type="text" />
                    <input onChange={e => {
                        this.setState({phone: e.target.value})
                    }} placeholder="Phone..." type="number" />
                    <button>Buy now to this address</button>
                </div>
            </div>
  1. 导出组件,以便将其导入路由器管理器,如以下代码所示:
export default Buy

我们只需要显示一个带有用户地址参数的表单,因为这是我们需要的唯一信息。运费,我们可以假设将全部免费,包括在价格中。我们将使用详细信息更新这个Buy组件的状态,以便我们稍后可以将该数据提交给智能合约。然后在索引文件的开头导入Buy组件。我突出显示了新的导入,让您看看Buy组件应该位于哪里,如下面的代码所示:

import React from 'react'
import ReactDOM from 'react-dom'
import MyWeb3 from 'web3'
import { BrowserRouter, Route, withRouter } from 'react-router-dom'
import Home from './components/Home'
import Product from './components/Product'
import Buy from './components/Buy'
import './index.styl'

然后将新的Routeprops参数添加到刚刚导入到render函数中的Buy组件中。这些更改会突出显示,以便您可以更快地找到它们,如以下代码所示:

class Main extends React.Component {
    // Omitted the other functions to keep it short

    render() {
        return (
            <div>
                <Route path="/" exact render={() => (
                    <Home
                        productsHtml={this.state.productsHtml}
                    />
                )} />
                <Route path="/product" render={() => (
                    <Product
                        product={this.state.product}
                        redirectTo={location => this.redirectTo(location)}
                    />
                )} />
 <Route path="/buy" render={() => (
 <Buy
 product={this.state.product}
 />
 )} />
            </div>
        )
    }
}

我们只需要将state.product发送到这个组件,这样我们就可以看到购买的是哪个产品。通过执行以下步骤,添加一些 CSS 代码使其看起来更好:

  1. 使用以下代码为Buy组件的产品部分添加 CSS 代码:
.product-buy-page
    display: grid
    margin: auto
    width: 50%
    padding: 20px
    padding-top: 0
    grid-template-columns: 50% 50%
    grid-template-rows: auto 1fr
    margin-bottom: 50px
    grid-column-gap: 40px
    border: 1px solid black
    background-color: white

    .title
        grid-column: 1 / 3
        justify-self: center

    .product-image
        grid-column: 1 / 2
        height: 150px
        justify-self: end

    .product-title
        margin-bottom: 25px

    .product-price
        font-size: 15pt
        font-weight: bold
  1. 添加Buy组件发货表单的 CSS 代码,如下面的代码所示:
.shipping-buy-page
    display: grid
    flex-direction: column
    justify-items: center
    width: 50%
    margin: auto
    margin-bottom: 200px

    input
        margin-bottom: 10px
        width: 100%

创建销售组件

我们正在建立一个分散的市场,全世界的用户都可以加入他们自己的产品,他们将免费发布。将不收取任何费用,购买将以加密货币完成。因此,我们需要为这些卖家创建一个专用页面,我们将通过以下步骤创建一个Sell组件:

  1. 导入必要的库来创建 React 组件并包含Header:
import React from 'react'
import Header from './Header'
  1. 使用一个空的构造函数创建Sell类,该构造函数包含带有用户将要销售的产品的titledescriptionimagepricestate对象,如下面的代码所示:
class Sell extends React.Component {
    constructor() {
        super()
        this.state = {
            title: '',
            description: '',
            price: '',
            image: '',
        }
    }
}
  1. 用简洁的形式创建render()函数,允许用户访问公共产品,如下面的代码所示。请注意图像是一个字符串,因为我们将对图像使用外部 URL,而不是自己托管文件:
render() {
    return (
        <div>
            <Header />
            <div className="sell-page">
                <h3>Sell product</h3>
                <input onChange={event => {
                    this.setState({title: event.target.value})
                }} type="text" placeholder="Product title..." />
                <textarea placeholder="Product description..." onChange={event => {
                    this.setState({description: event.target.value})
                }}></textarea>
                <input onChange={event => {
                    this.setState({price: event.target.value})
                }} type="text" placeholder="Product price in ETH..." />
                <input onChange={event => {
                    this.setState({image: event.target.value})
                }} type="text" placeholder="Product image URL..." />
                <p>Note that shipping costs are considered free so add the shipping price to the cost of the product itself</p>
                <button onClick={() => {
                    this.props.publishProduct(this.state)
                }} type="button">Publish product</button>
            </div>
        </div>
    )
}
  1. 使用以下代码导出这个新组件,以便其他文件可以导入它:
export default Sell

保存Sell组件后,将其导入到您的索引 JavaScript 文件中。我们必须添加一个名为publishProduct的函数,它将调用相应的智能合约函数。

以下步骤显示了导入此Sell组件所需的对索引文件(为清晰起见突出显示)的更改:

  1. Buy组件导入下导入Sell组件,如以下代码所示:
import React from 'react'
import ReactDOM from 'react-dom'
import MyWeb3 from 'web3'
import { BrowserRouter, Route, withRouter } from 'react-router-dom'
import Home from './components/Home'
import Product from './components/Product'
import Buy from './components/Buy'
import Sell from './components/Sell'
import './index.styl'
  1. Sell组件包含在具有自己的route对象的render()函数中,同时还定义了一个publishProduct()函数,如下面的函数所示:
class Main extends React.Component {
    // Omitted the other functions to keep it short

 async publishProduct(data) {}

    render() {
        return (
            <div>
                <Route path="/" exact render={() => (
                    <Home
                        productsHtml={this.state.productsHtml}
                    />
                )} />
                <Route path="/product" render={() => (
                    <Product
                        product={this.state.product}
                        redirectTo={location => this.redirectTo(location)}
                    />
                )} />
                <Route path="/buy" render={() => (
                    <Buy
                        product={this.state.product}
                    />
                )} /> <Route path="/sell" render={() => (
                    <Sell
                        publishProduct={data => this.publishProduct(data)}
                    />
                )} />
            </div>
        )
    }
}
  1. 添加一些 CSS 代码来改进此页面的设计,如函数所示:
.sell-page
    display: grid
    flex-direction: column
    justify-items: center
    width: 50%
    margin: auto
    margin-bottom: 200px

    input, textarea
        width: 100%
        margin-bottom: 10px

你可以通过点击标题中的Sell按钮看到它的样子,它重定向到/sell URL,加载Sell组件。

创建订单组件

通过以下步骤添加最后的Orders.js组件。在看到解决方案之前,尝试自己动手,这样你就可以用一些stylus CSS 来练习你的技能,从而完成设计。你会发现这比预期的要花更多的时间,但是这是值得的:

  1. 导入所需的库,如以下代码所示:
import React, { Component } from 'react'
import Header from './Header'
  1. 使用一些虚构的顺序来定义构造函数,以便您可以看到它的外观,如下面的代码所示:
class Orders extends Component {
    constructor() {
        super()

        // We'll separate the completed vs the pending based on the order state
        this.state = {
            sellOrders: [{
                id: 1,
                title: 'Classic trendy shoes',
                description: 'New unique shoes for sale',
                date: Date.now(),
                owner: '',
                price: 12,
                image: 'https://cdn.shopify.com/s/files/1/2494/8702/products/Bjakin-2018-Socks-Running-Shoes-for-Men-Lightweight-Sports-Sneakers-Colors-Man-Sock-Walking-Shoes-Big_17fa0d5b-d9d9-46a0-bdea-ac2dc17474ce_400x.jpg?v=1537755930',
                purchasedAt: Date.now(),
                state: 'completed',
            }],
            pendingSellOrdersHtml: [],
            pendingBuyOrdersHtml: [],
            completedSellOrdersHtml: [],
            completedBuyOrdersHtml: [],
        }

        this.displayOrders()
    }
  1. 我们需要一个函数,通过从智能合同中获取数据来获取用户的订单,同时还将订单标记为已完成。我们还不会实现这些功能,因为我们必须首先创建智能合约,如以下代码所示:
    async getUserOrders() {}

    async markAsCompleted(product) {}
  1. 添加这些空函数,然后创建一个名为displayOrders()的函数,它将获取状态数据以输出结果 HTML。首先定义内部使用的数组,如下面的代码所示:
async displayOrders() {
    let pendingSellOrdersHtml = []
    let pendingBuyOrdersHtml = []
    let completedSellOrdersHtml = []
    let completedBuyOrdersHtml = []
}
  1. 读取不同的 order 对象以遍历它们并生成有效的 JSX。根据产品的状态对产品进行分类,如下面的代码所示:
await this.state.sellOrders.asyncForEach(product => {
    if(product.state == 'pending') {
        pendingSellOrdersHtml.push(
            <div key={product.id} className="product">
                <img className="product-image" src={product.image} />
                <div className="product-data">
                    <h3 className="small-product-title">{product.title}</h3>
                    <div className="product-state">State: {product.state}</div>
                    <div className="product-description">{product.description.substring(0, 15) + '...'}</div>
                    <div className="product-price">{product.price} ETH</div>
                    <button className="small-view-button" onClick={() => {
                        this.props.setState({product})
                        this.props.redirectTo('/product')
                    }} type="button">View</button>
                    <button className="small-completed-button" onClick={() => {
                        this.markAsCompleted(product)
                    }} type="button">Mark as completed</button>
                </div>
            </div>
        )
  1. 如果销售订单的状态是 completed,那么将其推入到completedSellOrders数组中,因为我们希望根据订单的状态对其进行分类,如下面的代码所示。创建一个新的 HTML 块,因为它会略有不同,因为我们想使用一个按钮来标记产品已完成:
} else {
        completedSellOrdersHtml.push(
            <div key={product.id} className="product">
                <img className="product-image" src={product.image} />
                <div className="product-data">
                    <h3 className="product-title">{product.title}</h3>
                    <div className="product-state">State: {product.state}</div>
                    <div className="product-description">{product.description.substring(0, 15) + '...'}</div>
                    <div className="product-price">{product.price} ETH</div>
                    <button onClick={() => {
                        this.props.setState({product})
                        this.props.redirectTo('/product')
                    }} className="product-view" type="button">View</button>
                </div>
            </div>
        )
    }
})
  1. buyOrders数组使用相同的过程来设计每个产品的 HTML,同时遍历数组,如以下代码所示:
await this.state.buyOrders.asyncForEach(product => {
    let html = (
        <div key={product.id} className="product">
            <img className="product-image" src={product.image} />
            <div className="product-data">
                <h3 className="product-title">{product.title}</h3>
                <div className="product-state">State: {product.state}</div>
                <div className="product-description">{product.description.substring(0, 15) + '...'}</div>
                <div className="product-price">{product.price} ETH</div>
                <button onClick={() => {
                    this.props.setState({product})
                    this.props.redirectTo('/product')
                }} className="product-view" type="button">View</button>
            </div>
        </div>
    )

    if(product.state == 'pending') pendingBuyOrdersHtml.push(html)
    else completedBuyOrdersHtml.push(html)
})
  1. 用生成的 HTML 对象更新组件的状态,如下面的代码所示:
this.setState({pendingSellOrdersHtml, pendingBuyOrdersHtml, completedSellOrdersHtml, completedBuyOrdersHtml})
  1. 创建render()函数来显示这些生成的订单,如下面的代码所示:
    render() {
        return (
            <div>
                <Header />
                <div className="orders-page">
                    <div>
                        <h3 className="order-title">PENDING ORDERS AS A SELLER</h3>
                        {this.state.pendingSellOrdersHtml}
                    </div>
                    <div>
                        <h3 className="order-title">PENDING ORDERS AS A BUYER</h3>
                        {this.state.pendingBuyOrdersHtml}
                    </div>
                    <div>
                        <h3 className="order-title">COMPLETED SELL ORDERS</h3>
                        {this.state.completedSellOrdersHtml}
                    </div>
                    <div>
                        <h3 className="order-title">COMPLETED BUY ORDERS</h3>
                        {this.state.completedBuyOrdersHtml}
                    </div>
                </div>
            </div>
        )
    }
}
  1. 导出Orders组件对象,如下面的代码所示:
export default Orders

这是一大段代码,因为我们在 state 对象中添加了一些示例订单数据,以显示订单页面的真实视图。您可以看到,我们为每个产品添加了一个state属性,它向我们显示订单是待定还是已完成。这将在智能合同中设置。displayOrders函数为每种类型的订单生成 HTML 对象,因为我们希望将已完成和未完成的订单以及买卖订单分开,以便您可以看到所有重要的信息。智能合同实施后,订单将来自getUserOrders功能。加一些 CSS,让它看起来体面一些。你可以在 https://github.com/merlox/ecommerce-dapp的官方 GitHub文件夹里查看我的设计。

最后,您将得到一个看起来很酷的订单页面,如下面的屏幕截图所示:

谈到 React 中的用户界面,大概就是这样了!为了确保安全,一旦创建了所有组件,您的src/文件夹中应该有以下文件:

  • components/
    • Buy.js
    • Header.js
    • Sell.js
    • Product.js
    • Home.js
    • Orders.js
  • index.ejs
  • index.js
  • index.styl

了解 ERC-721 令牌

这种新型令牌用于在我们的智能合约中生成独特的产品。ERC-721 标准已经得到官方以太坊团队的批准,这意味着你将能够在各种应用程序中使用它,因为它将与依赖于该标准的工具和智能合约兼容。正如 ERC-20 代币催生了分散代币交易所一样,我们可以期待分散 ERC-721 交易所以及数字和实物产品市场的诞生。

解释 ERC 721 的功能

为了理解 ERC-721 令牌是如何工作的,最好看一下定义 ERC-721 令牌的函数,这样就可以理解它们在内部是如何工作的。以下列表描述了这些功能:

  • balanceOf(owner):返回用户拥有的给定地址的所有令牌的计数。
  • ownerOf(tokenId):返回拥有特定令牌 ID 的地址。

  • 给一点零用钱后,从一个地址发送一个代币到另一个地址,就像这个短语处理 ERC-20 代币一样。它之所以被称为安全,是因为如果接收者是一个契约,它会检查该契约是否能够接收 ERC-721 令牌,这意味着接收者契约已经实现了onERC721Received函数,这样您就不会将令牌丢失给一个无法管理这些类型令牌的契约。可以省略data参数,它只包含您可能想要发送到to接收器地址的额外字节信息。from地址必须是当前的所有者,因此您可以将此函数用作普通的transfer函数或transferFrom函数(您可能在使用 ERC-20 令牌时已经熟悉了该函数),用于批准向另一个地址发送令牌。

  • transferFrom(from, to, tokenId):这与前面的函数相同,但是它不能确保接收者地址能够管理这些类型的令牌,如果它被证明是智能合约的话。
  • 这用于将特定的令牌批准给另一个所有者,这样他们就可以随心所欲地使用它。
  • setApprovalForAll(operator, approved):这是为你的所有代币创建一个限额到另一个地址,称为operator地址,可以管理你的全部余额。您可以通过将approved参数设置为false来取消对特定操作员的访问。
  • getApproved(tokenId):返回允许该令牌的地址。
  • isApprovedForAll(owner, operator):如果operator可以访问所有所有者的令牌,则返回true

请注意他们是如何从 ERC-20 规范中删除我们熟悉的transfer函数的,因为它通过允许将transferFromsafeTransferFrom函数用作正常传输或批准的传输来简化过程,从而消除了对标准transfer函数的需要。

_mint(owner, tokenId)_burn(tokenId)内部函数用于生成和删除令牌;然而,它们在标准的ERC721.sol智能契约中不可用,因为它们是内部的,这意味着它们需要您创建一个新的契约来继承 ERC-721 契约,并实现定制的mint(owner, tokenId)burn(tokenId)函数(没有下划线),并进行您可能需要的任何修改,因为我们希望限制谁可以创建或删除令牌。

你能想象每个人都能随心所欲地生成代币吗?这就违背了拥有有价值的代币的目的,所以这就是为什么他们强迫你创建你自己的铸造函数,并限制访问,很可能带有一个onlyOwner修饰符。在我们的案例中,我们将允许卖家为他们的产品铸造新型的 ERC-721 代币。

我们分散的电子商务商店中的每个产品都将代表一个独特的 ERC-721;这就是为什么我们不想为每个产品添加多个数量,因为我们必须创建几个 ERC-721 的唯一实例。另一方面,NFT 意味着每个令牌都有其独特的区分属性。与每个令牌都相同的 ERC-20 相比,ERC-721 标准旨在用于独特的项目,如家庭用品、手工制品、艺术品或独特的数字资产,如游戏皮肤。有趣的是,您可以将这两种标准结合起来创建独特的令牌,同时还能够生成同一令牌的多个实例。

ERC-721 智能合同

既然您已经理解了这些类型的非功能性测试是如何工作的,让我们来看看 ERC-721 合同接口。该实现可在 GitHub 上的https://GitHub . com/merlox/ecommerce-dapp/blob/master/contracts/ERC 721 . sol获得,因为完整代码太大,无法在此显示:

pragma solidity ^0.5.0;

contract IERC721{
    event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
    event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);
    event ApprovalForAll(address indexed owner, address indexed operator, bool approved);
    function balanceOf(address owner) public view returns (uint256 balance);
    function ownerOf(uint256 tokenId) public view returns (address owner);
    function approve(address to, uint256 tokenId) public;
    function getApproved(uint256 tokenId) public view returns (address operator);
    function setApprovalForAll(address operator, bool _approved) public;
    function isApprovedForAll(address owner, address operator) public view returns (bool);
    function transferFrom(address from, address to, uint256 tokenId) public;
    function safeTransferFrom(address from, address to, uint256 tokenId) public;
    function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory data) public;
}

该合同与 ERC-20 合同非常相似,因为它们背后的基本思想是相同的。该契约用于生成许多具有挖掘功能的唯一令牌,挖掘功能必须单独实现,因为您希望控制谁能够创建令牌,谁能够销毁令牌。

在您的contracts/文件夹中创建一个名为ERC721.sol的文件,并添加该代码,因为我们稍后会用到它。我们将创建一个继承 ERC-721 智能契约的契约来实现mint()功能,因为默认的 ERC-721 实现不能访问它。使用下面的代码创建一个名为Ecommerce.sol的新文件,并在那里导入ERC721.sol合同:

pragma solidity ^0.5.0;

import './ERC721.sol';

只要功能相同,坚固性版本并不重要。创建您自己的 ERC-721 智能协定的自定义实现,它继承了这个实现,如下所示:

pragma solidity ^0.5.0;

import './ERC721.sol';

/// @notice The Ecommerce Token that implements the ERC721 token with mint function
/// @author Merunas Grincalaitis <merunasgrincalaitis@gmail.com>
contract EcommerceToken is ERC721 {
 address public ecommerce;
 bool public isEcommerceSet = false;
    /// @notice To generate a new token for the specified address
    /// @param _to The receiver of this new token
    /// @param _tokenId The new token id, must be unique
 function mint(address _to, uint256 _tokenId) public {
 require(msg.sender == ecommerce, 'Only the ecommerce contract can mint new tokens');
 _mint(_to, _tokenId);
 }

    /// @notice To set the ecommerce smart contract address
 function setEcommerce(address _ecommerce) public {
 require(!isEcommerceSet, 'The ecommerce address can only be set once');
 require(_ecommerce != address(0), 'The ecommerce address cannot be empty');
 isEcommerceSet = true;
 ecommerce = _ecommerce;
 }
}

这种代币合约只会让电商合约产生新的代币,在购买完成后转移给买家;在您能够铸造代币之前,必须设置好setEcommerce功能。

开发电子商务智能合同

开发与 ERC-721 令牌交互的智能合约很简单,因为我们只需确保用户拥有与其产品相关联的令牌 ID。如果用户愿意,他们将能够独立地与他们的令牌进行交互。对于我们的市场,我们将重点创建 buy 和 sell 函数来创建和燃烧令牌。像往常一样,我们还将创建多个 getters 来从用户界面的智能契约中提取数据。

让我们开始创建电子商务合同,它将所有的市场逻辑放在同一个文件中,因为它不会占用太多空间:

  1. 定义智能协定所需的变量,从您将需要的结构开始,如下面的代码所示:
/// @notice The main ecommerce contract to buy and sell ERC-721 tokens representing physical or digital products because we are dealing with non-fungible tokens, there will be only 1 stock per product
/// @author Merunas Grincalaitis <merunasgrincalaitis@gmail.com>
contract Ecommerce {
    struct Product {
        uint256 id;
        string title;
        string description;
        uint256 date;
        address payable owner;
        uint256 price;
        string image;
    }
    struct Order {
        uint256 id;
        address buyer;
        string nameSurname;
        string lineOneDirection;
        string lineTwoDirection;
        bytes32 city;
        bytes32 stateRegion;
        uint256 postalCode;
        bytes32 country;
        uint256 phone;
        string state; // Either 'pending', 'completed'
    }
  1. 添加映射、数组、变量和构造函数,如以下代码所示:
    // Seller address => products
    mapping(address => Order[]) public pendingSellerOrders; // The products waiting to be fulfilled by the seller, used by sellers to check which orders have to be filled
    // Buyer address => products
    mapping(address => Order[]) public pendingBuyerOrders; // The products that the buyer purchased waiting to be sent
    mapping(address => Order[]) public completedOrders;
    // Product id => product
    mapping(uint256 => Product) public productById;
    // Product id => order
    mapping(uint256 => Order) public orderById;
    Product[] public products;
    uint256 public lastId;
    address public token;

    /// @notice To setup the address of the ERC-721 token to use for this contract
    /// @param _token The token address
    constructor(address _token) public {
        token = _token;
    }
}

我们必须首先从结构开始设置变量,在本例中是ProductOrder。每个订单将通过 ID 引用一个特定的产品,这在两种情况下是相同的,这意味着每个产品都有一个具有相同 ID 的相应订单。对于尚未完成的未决订单将有映射,对于已经完成的订单将有其他映射,因此我们有一个已完成订单的引用。构造函数将接收令牌地址,以便电子商务合同可以创建新的令牌。

创建发布功能

使用以下代码创建一个发布新产品的函数,以便用户可以自行销售产品。图像 URL 将是图像所在的位置:

/// @notice To publish a product as a seller
/// @param _title The title of the product
/// @param _description The description of the product
/// @param _price The price of the product in ETH
/// @param _image The image URL of the product
function publishProduct(string memory _title, string memory _description, uint256 _price, string memory _image) public {
    require(bytes(_title).length > 0, 'The title cannot be empty');
    require(bytes(_description).length > 0, 'The description cannot be empty');
    require(_price > 0, 'The price cannot be empty');
    require(bytes(_image).length > 0, 'The image cannot be empty');

    Product memory p = Product(lastId, _title, _description, now, msg.sender, _price, _image);
    products.push(p);
    productById[lastId] = p;
    EcommerceToken(token).mint(address(this), lastId); // Create a new token for this product which will be owned by this contract until sold
    lastId++;
}

该函数将检查参数,以便在创建新令牌的同时设置这些参数。

创建购买功能

既然用户可以发布要销售的产品,您可以使用buy功能来购买产品:

/// @notice To buy a new product, note that the seller must authorize this contract to manage the token
/// @param _id The id of the product to buy
/// @param _nameSurname The name and surname of the buyer
/// @param _lineOneDirection The first line for the user address
/// @param _lineTwoDirection The second, optional user address line
/// @param _city Buyer's city
/// @param _stateRegion The state or region where the buyer lives
/// @param _postalCode The postal code of his location
/// @param _country Buyer's country
/// @param _phone The optional phone number for the shipping company
function buyProduct(uint256 _id, string memory _nameSurname, string memory _lineOneDirection, string memory _lineTwoDirection, bytes32 _city, bytes32 _stateRegion, uint256 _postalCode, bytes32 _country, uint256 _phone) public payable {
    // The line 2 address and phone are optional, the rest are mandatory
    require(bytes(_nameSurname).length > 0, 'The name and surname must be set');
    require(bytes(_lineOneDirection).length > 0, 'The line one direction must be set');
    require(_city.length > 0, 'The city must be set');
    require(_stateRegion.length > 0, 'The state or region must be set');
    require(_postalCode > 0, 'The postal code must be set');
    require(_country > 0, 'The country must be set');

    Product memory p = productById[_id];
    require(bytes(p.title).length > 0, 'The product must exist to be purchased');
    Order memory newOrder = Order(_id, msg.sender, _nameSurname, _lineOneDirection, _lineTwoDirection, _city, _stateRegion, _postalCode, _country, _phone, 'pending');
    require(msg.value >= p.price, "The payment must be larger or equal than the products price");

    // Delete the product from the array of products
    for(uint256 i = 0; i < products.length; i++) {
        if(products[i].id == _id) {
            Product memory lastElement = products[products.length - 1];
            products[i] = lastElement;
            products.length--;
        }
    }

    // Return the excess ETH sent by the buyer
    if(msg.value > p.price) msg.sender.transfer(msg.value - p.price);
    pendingSellerOrders[p.owner].push(newOrder);
    pendingBuyerOrders[msg.sender].push(newOrder);
    orderById[_id] = newOrder;
    EcommerceToken(token).transferFrom(address(this), msg.sender, _id); // Transfer the product token to the new owner
    p.owner.transfer(p.price);
}

首先,buy函数必须是可支付的,以便用户可以在以太坊中发送所需的价格,这些价格将被发送到卖方,除了气体成本之外,没有任何费用。购买产品时,买家需要发送所有的地址详情,以便卖家处理发货;这就是为什么在buy函数中有这么多参数,其中电话号码和第二个地址行是可选的。products数组删除产品,以便用户界面显示最新的产品。一个新的order struct 实例将被创建,订单将被添加到挂起的映射中。

创建标记订单功能

创建订单后,我们需要一种方法来告诉客户产品已经发货。我们可以使用一个名为markOrderCompleted的新函数来实现,如下面的代码所示:

/// @notice To mark an order as completed
/// @param _id The id of the order which is the same for the product id
function markOrderCompleted(uint256 _id) public {
    Order memory order = orderById[_id];
    Product memory product = productById[_id];
    require(product.owner == msg.sender, 'Only the seller can mark the order as completed');
    order.state = 'completed';

    // Delete the seller order from the array of pending orders
    for(uint256 i = 0; i < pendingSellerOrders[product.owner].length; i++) {
        if(pendingSellerOrders[product.owner][i].id == _id) {
            Order memory lastElement = orderById[pendingSellerOrders[product.owner].length - 1];
            pendingSellerOrders[product.owner][i] = lastElement;
            pendingSellerOrders[product.owner].length--;
        }
    }
    // Delete the seller order from the array of pending orders
    for(uint256 i = 0; i < pendingBuyerOrders[order.buyer].length; i++) {
        if(pendingBuyerOrders[order.buyer][i].id == order.id) {
            Order memory lastElement = orderById[pendingBuyerOrders[order.buyer].length - 1];
            pendingBuyerOrders[order.buyer][i] = lastElement;
            pendingBuyerOrders[order.buyer].length--;
        }
    }
    completedOrders[order.buyer].push(order);
    orderById[_id] = order;
}

该函数从各自的数组中删除挂单,并将它们移动到completedOrders映射中。我们不使用删除函数,而是减少数组的长度来删除Order,因为delete函数并没有真正从数组中删除用户订单,而是在其位置留下一个空订单实例。当我们将我们想要delete的元素移动到数组的最后一个位置并减少它的长度时,我们完全删除了它,没有留下任何空洞,因为delete函数保持了数组的完整性。

创建 getter 函数

剩下的就是添加所需的getter函数来返回这些数组的长度,因为公共数组变量不公开数组长度,我们需要知道有多少产品和订单向用户显示最新的内容,让我们使用下面的代码来设置它:

/// @notice Returns the product length
/// @return uint256 The number of products
function getProductsLength() public view returns(uint256) {
    return products.length;
}

/// @notice To get the pending seller or buyer orders
/// @param _type If you want to get the pending seller, buyer or completed orders
/// @param _owner The owner of those orders
/// @return uint256 The number of orders to get
function getOrdersLength(bytes32 _type, address _owner) public view returns(uint256) {
    if(_type == 'seller') return pendingSellerOrders[_owner].length;
    else if(_type == 'buyer') return pendingBuyerOrders[_owner].length;
    else if(_type == 'completed') return completedOrders[_owner].length;
}

getOrdersLength()函数将用于所有三种类型的订单,卖方、买方或已完成订单,以避免创建多个类似的函数。这是整个合同。如果你想看更新的版本,请访问我的 GitHub:https://github.com/merlox/ecommerce-dapp

部署智能合同

理解部署过程以保证成功执行是很重要的,因为,让我们面对现实吧,Truffle 可能很难设置。在前面的章节中,您已经看到了使用这个框架部署一个智能契约需要做些什么,但是为了确保您理解了它,再回顾一遍这个过程是没有坏处的。

首先,打开您的truffle-config.js文件并为ropsten修改它,这是我们将用来部署 dApp 初始版本的网络。使用您自己的 INFURA 键,它应该是这样的:

const HDWalletProvider = require('truffle-hdwallet-provider');
const infuraKey = "v3/<YOUR-INFURA-KEY-HERE>;
const fs = require('fs');
const mnemonic = fs.readFileSync(".secret").toString().trim();

module.exports = {
  networks: {
    development: {
     host: "127.0.0.1", // Localhost (default: none)
     port: 8545, // Standard Ethereum port (default: none)
     network_id: "*", // Any network (default: none)
    },
    ropsten: {
      provider: () => new HDWalletProvider(mnemonic, `https://ropsten.infura.io/${infuraKey}`),
      network_id: 3, // Ropsten's id
      gas: 5500000, // Ropsten has a lower block limit than mainnet
      confirmations: 2, // # of confs to wait between deployments. (default: 0)
      timeoutBlocks: 200, // # of blocks before a deployment times out (minimum/default: 50)
      skipDryRun: true // Skip dry run before migrations? (default: false for public nets )
    }
  }
}

我让开发网络可用,因为在将合同部署到ropsten之前,您可能需要在用ganache-cli生成的本地测试网上检查部署过程。确保你有足够的ropsten以太币在你的.secret文件中用种子短语生成的第一个账户中。记得安装 Truffle wallet,以便部署过程使用以下代码:

npm i -S truffle-hdwallet-provider

然后,用您的种子短语创建一个.secret文件,并在您的migrations/文件夹中创建一个名为2_deploy_contracts.js的文件,告诉 Truffle 在部署合同时需要做什么,主要用于设置构造函数参数,如下面的代码所示。如果部署时没有此文件,Truffle 将会失败:

const Token = artifacts.require("./EcommerceToken.sol")
const Ecommerce = artifacts.require("./Ecommerce.sol")
let token

module.exports = function(deployer, network, accounts) {
    deployer.deploy(
        Token,
        { gas: 8e6 }
    ).then(tokenInstance => {
        token = tokenInstance
        return deployer.deploy(Ecommerce, token.address, {
            gas: 8e6
        })
    }).then(async ecommerce => {
        await token.contract.methods.setEcommerce(ecommerce.address).send({
            from: accounts[0]
        })
        console.log('Is set?', await token.contract.methods.isEcommerceSet().call())
        console.log('Deployed both!')
    })
}

您的迁移文件夹应该有1_initial_migrations.js2_deploy_contracts.js文件。语法有点混乱,但重要的是我们正在使用deployer.deploy()函数,它返回一个承诺,从令牌契约中获取令牌地址并运行setEcommerce()函数,以便我们可以立即开始使用契约。注意我们如何通过向主函数添加第三个参数来访问accounts;这是使用第一个以太坊地址运行setEcommerce()功能所必需的。最后,我通过从令牌中调用isEcommerceSet()公共变量来检查电子商务合同是否已经在令牌中正确设置。

运行以下部署命令:

truffle deploy --network ropsten --reset

如果您想测试一切运行正常,而不需要等待ropsten,您可以启动一个ganache-cli私有区块链,并通过运行下面的命令行立即部署它:

truffle deploy --network development --reset

部署合同后,您将在build/contract/Ecommerce.json文件夹中找到地址和 ABI。

完成 dApp

为了完成 dApp,我们必须修改 React 代码以集成智能合同更改,同时还要理解我们从区块链接收信息的方式,使用正确的方法正确显示数据。在此之前,确保您的合同被部署到ropsten,如前面的步骤所示。

设置合同实例

因为我们使用 webpack,所以我们可以从 React 文件访问源文件夹中的所有文件,这意味着我们可以获得部署的智能合约 ABI 和部署的合约地址,以及创建合约实例所需的参数。这显示在以下代码中:

import React from 'react'
import ReactDOM from 'react-dom'
import MyWeb3 from 'web3'
import { BrowserRouter, Route, withRouter } from 'react-router-dom'
import Home from './components/Home'
import Product from './components/Product'
import Sell from './components/Sell'
import Header from './components/Header'
import Buy from './components/Buy'
import Orders from './components/Orders'

import './index.styl'
import ABI from '../build/contracts/Ecommerce.json'

当您使用 Truffle 成功部署您的智能合约时,将会创建一个build文件夹,其中包含我们的 dApp 可能需要的重要智能合约参数。修改您的设置函数,使其能够全局访问合同对象,从而简化外部组件的工作。我在下面的代码中突出显示了契约实例,以便您找到更改:

async setup() {
    // Create the contract instance
    window.myWeb3 = new MyWeb3(ethereum)
    try {
        await ethereum.enable();
    } catch (error) {
        console.error('You must approve this dApp to interact with it')
    }
 window.user = (await myWeb3.eth.getAccounts())[0]
 window.contract = new myWeb3.eth.Contract(ABI.abi, ABI.networks['3'].address, {
 from: user
 })
    await this.getLatestProducts(9)
    await this.displayProducts()
}

请注意我们是如何将state对象减少到几个没有任何虚拟数据的元素,因为我们将使用真正的智能合约数据。契约实例是通过使用abi和契约地址创建的,契约地址也包含在构建 JSON 文件中。在 setup 函数的最后,我们调用了getLatestProducts()displayProducts()函数,您马上就会看到,这些函数对于从契约中获取数据并正确显示是必要的。

更新索引文件

现在我们有了一个可工作的契约实例,我们可以处理索引文件所需的功能,这样我们就可以将功能包含在较小的组件中,如下面的代码所示:

  1. 实现displayProducts()函数来显示按属性排序的产品:
async displayProducts() {
    let productsHtml = []
    if(this.state.products.length == 0) {
        productsHtml = (
            <div key="0" className="center">There are no products yet...</div>
        )
    }
    await this.state.products.asyncForEach(product => {
        productsHtml.push((
            <div key={product.id} className="product">
                <img className="product-image" src={product.image} />
                <div className="product-data">
                    <h3 className="product-title">{product.title}</h3>
                    <div className="product-description">{product.description.substring(0, 50) + '...'}</div>
                    <div className="product-price">{product.price} ETH</div>
                    <button onClick={() => {
                        this.setState({product})
                        this.redirectTo('/product')
                    }} className="product-view" type="button">View</button>
                </div>
            </div>
        ))
    })
    this.setState({productsHtml})
}
  1. 添加更新后的重定向函数,如以下代码所示:
redirectTo(location) {
  this.props.history.push({
    pathname: location
  })
}
  1. 通过获取产品的长度并循环每个产品,实现从智能合约中获取产品的函数:
async getLatestProducts(amount) {
    // Get the product ids
    const productsLength = parseInt(await contract.methods.getProductsLength().call())
    let products = []
    let condition = (amount > productsLength) ? 0 : productsLength - amount

    // Loop through all of them one by one
    for(let i = productsLength; i > condition; i--) {
        let product = await contract.methods.products(i - 1).call()
        product = {
            id: parseInt(product.id),
            title: product.title,
            date: parseInt(product.date),
            description: product.description,
            image: product.image,
            owner: product.owner,
            price: myWeb3.utils.fromWei(String(product.price)),
        }
        products.push(product)
    }
    this.setState({products})
}

在我们的主页上,我们将展示其他卖家添加的最新产品,以便您可以立即开始购买。因此,我们将使用getLatestProducts(),它接收要显示的产品数量作为参数,同时从区块链获取数据。没有一个getter函数,我们如何获得所有的产品数据?这个过程是这样的:

  1. 我们得到产品数组的长度。我们使用getProductsLength()函数,因为没有合适的getter函数,我们无法获得数组的长度。
  2. 一旦我们知道智能合约中有多少产品可用,我们就循环遍历该大小以运行products()函数,该函数可用是因为我们的产品数组是公共的,这意味着它有一个为其自动创建的getter函数。公共数组必须逐个访问;这就是为什么我们使用一个反向的for回路。
  3. 我们需要一个反向循环,先获取最新产品。for循环是如何工作的,因为如果我们想显示9时,我们正好从零个产品开始,这可能是我们用完了要显示的产品的情况,在设置功能的末尾有所指示。这就是为什么我们创建了condition变量——它检查请求显示的产品数量是否实际可用;如果没有,我们只是得到所有可用的产品,不管它们有多少。

另一方面,一旦用包含在我们的智能契约中的产品填充了state对象,我们就使用displayProducts()函数,它负责生成每个产品所需的适当的 HTML,同时更新productsHtml状态数组。

最后,我们有render函数,它已经针对这些新的更新组件进行了轻微的修改,如下面的代码所示:

render() {
    return (
        <div>
            <Route path="/product" render={() => (
                <Product
                    product={this.state.product}
                    redirectTo={location => this.redirectTo(location)}
                />
            )}/>
            <Route path="/sell" render={() => (
                <Sell
                    publishProduct={data => this.publishProduct(data)}
                />
            )}/>
            <Route path="/buy" render={() => (
                <Buy
                    product={this.state.product}
                />
            )} />
            <Route path="/orders" render={() => (
                <Orders
                    setState={state => this.setState(state)}
                    redirectTo={location => this.redirectTo(location)}
                />
            )} />
            <Route path="/" exact render={() => (
                <Home
                    productsHtml={this.state.productsHtml}
                />
            )} />
        </div>
    )
}

在进行了实现更改之后,看看整个索引文件,可以在 GitHub 的https://github.com/merlox/ecommerce-dapp获得。

更新采购组件

让我们转到Buy.js文件,因为Home.jsProduct.js组件将保持原样,无需任何修改,考虑到产品数据将具有相同的预期格式。在Buy组件中,我们需要添加一个购买产品的函数,该函数会将交易发送到智能合约,该函数如下:

async buyProduct() {
    await contract.methods.buyProduct(this.props.product.id, this.state.nameSurname, this.state.lineOneDirection, this.state.lineTwoDirection, this.bytes32(this.state.city), this.bytes32(this.state.stateRegion), this.state.postalCode, this.bytes32(this.state.country), this.state.phone).send({
        value: myWeb3.utils.toWei(this.props.product.price)
    })
}

bytes32(name) {
    return myWeb3.utils.fromAscii(name)
}

buyProduct()函数获取关于用户地址的所有状态数据,并发送带有所需产品价格的交易,作为交易的付款。需要使用bytes32函数将一些字符串值转换为字节 32,以节省开销。这就是这个特定组件所需的全部更改。在更新后的 GitHub 上查看整个组件的最终实现:https://GitHub . com/merlox/ecommerce-dapp/blob/master/src/components/buy . js

更新销售组件

让我们为Sell.js函数创建所需的功能,这样您就可以开始向市场添加可购买的产品。在这种情况下,我们需要添加一个从智能契约中调用publishProduct()函数的函数。下面是更新后的 publish功能的样子:

async publishProduct() {
    if(this.state.title.length == 0) return alert('You must set the title before publishing the product')
    if(this.state.description.length == 0) return alert('You must set the description before publishing the product')
    if(this.state.price.length == 0) return alert('You must set the price before publishing the product')
    if(this.state.image.length == 0) return alert('You must set the image URL before publishing the product')

    await contract.methods.publishProduct(this.state.title, this.state.description, myWeb3.utils.toWei(this.state.price), this.state.image).send()
}

请注意,我们是如何检查所有必需的参数的,以便让用户知道什么东西丢失了。您可以添加一些额外的检查,以确保所提供的图片 URL 确实是可以在市场上显示的有效图片。我将把那件事留给你。这不会花费你超过 10 分钟的时间,而且这是一个很好的练习你的 JavaScript 技能的机会。

最终更新版本可在 GitHub 上获得:https://GitHub . com/merlox/ecommerce-dapp/blob/master/src/components/sell . js

更新订单组件

现在让我们更新Orders.js组件,这是最复杂的组件,因为我们必须生成多个产品。让我们首先创建一个函数来获取与当前用户相关的所有订单,如下面的代码所示:

async getOrders(amount) {
    const pendingSellerOrdersLength = parseInt(await contract.methods.getOrdersLength(this.bytes32('seller'), user).call())
    const pendingBuyerOrdersLength = parseInt(await contract.methods.getOrdersLength(this.bytes32('buyer'), user).call())
    const completedOrdersLength = parseInt(await contract.methods.getOrdersLength(this.bytes32('completed'), user).call())

    const conditionSeller = (amount > pendingSellerOrdersLength) ? 0 : pendingSellerOrdersLength - amount
    const conditionBuyer = (amount > pendingBuyerOrdersLength) ? 0 : pendingBuyerOrdersLength - amount
    const conditionCompleted = (amount > completedOrdersLength) ? 0 : completedOrdersLength - amount

    let pendingSellerOrders = []
    let pendingBuyerOrders = []
    let completedOrders = []

    // In reverse to get the most recent orders first
    for(let i = pendingSellerOrdersLength; i > conditionSeller; i--) {
        let order = await contract.methods.pendingSellerOrders(user, i - 1).call()
        pendingSellerOrders.push(await this.generateOrderObject(order))
    }

    for(let i = pendingBuyerOrdersLength; i > conditionBuyer; i--) {
        let order = await contract.methods.pendingBuyerOrders(user, i - 1).call()
        pendingBuyerOrders.push(await this.generateOrderObject(order))
    }

    for(let i = completedOrdersLength; i > conditionCompleted; i--) {
        let order = await contract.methods.completedOrders(user, i - 1).call()
        completedOrders.push(await this.generateOrderObject(order))
    }

    this.setState({pendingSellerOrders, pendingBuyerOrders, completedOrders})
}

我们按照索引文件中用于产品的相同过程生成三个不同的数组。我们有相同的条件运算符,但用于不同类型的订单。然后,我们对每个期望的订单反向运行一个for循环,以便获得最新的订单。因为智能契约返回的数据有点混乱,所以我们创建了一个名为generateOrderObject()的函数,它接收一个 order 对象,并返回一个经过清理的对象,该对象包含已转换为可读文本的十六进制值。它看起来是这样的:

async generateOrderObject(order) {
    let productAssociated = await contract.methods.productById(parseInt(order.id)).call()
    order = {
        id: parseInt(order.id),
        buyer: order.buyer,
        nameSurname: order.nameSurname,
        lineOneDirection: order.lineOneDirection,
        lineTwoDirection: order.lineTwoDirection,
        city: myWeb3.utils.toUtf8(order.city),
        stateRegion: myWeb3.utils.toUtf8(order.stateRegion),
        postalCode: String(order.postalCode),
        country: myWeb3.utils.toUtf8(order.country),
        phone: String(order.phone),
        state: order.state,
        date: String(productAssociated.date),
        description: productAssociated.description,
        image: productAssociated.image,
        owner: productAssociated.owner,
        price: myWeb3.utils.fromWei(String(productAssociated.price)),
        title: productAssociated.title,
    }
    return order
}

在外部函数中分离重复代码以保持代码整洁是很重要的。正如您所看到的,这个函数将变量的字节类型转换成可读的utf8字符串,同时也将 BigNumbers 转换成整数,以便它们可以正确地显示在我们的用户界面上。

在用最近的订单更新状态对象后,我们可以通过以下步骤创建一个函数来为每个元素生成适当的 HTML:

  1. 设置所需的数组变量,这在本例中更简单,因为我们要为不同类型的订单创建三个块:
async displayOrders() {
    let pendingSellerOrdersHtml = []
    let pendingBuyerOrdersHtml = []
    let completedOrdersHtml = []
  1. 如果每种类型的订单都没有订单,我们希望通过使用以下代码显示一条消息,让用户知道没有订单:
    if(this.state.pendingSellerOrders.length == 0) {
        pendingSellerOrdersHtml.push((
            <div key="0" className="center">There are no seller orders yet...</div>
        ))
    }
    if(this.state.pendingBuyerOrders.length == 0) {
        pendingBuyerOrdersHtml.push((
            <div key="0" className="center">There are no buyer orders yet...</div>
        ))
    }
    if(this.state.completedOrders.length == 0) {
        completedOrdersHtml.push((
            <div key="0" className="center">There are no completed orders yet...</div>
        ))
    }
  1. 通过使用以下代码添加地址部分来更新未决订单:
    await this.state.pendingSellerOrders.asyncForEach(order => {
        pendingSellerOrdersHtml.push(
            <div key={order.id} className="product">
                <img className="product-image" src={order.image} />
                <div className="product-data">
                    <h3 className="small-product-title">{order.title}</h3>
                    <div className="product-state">State: {order.state}</div>
                    <div className="product-description">{order.description.substring(0, 15) + '...'}</div>
                    <div className="product-price">{order.price} ETH</div>
                    <button className="small-view-button" onClick={() => {
                        this.props.setState({product: order})
                        this.props.redirectTo('/product')
                    }} type="button">View</button>
                    <button className="small-completed-button" onClick={() => {
                        this.markAsCompleted(order.id)
                    }} type="button">Mark as completed</button>
                </div>
  1. 在产品数据的正下方,添加地址信息,以便销售人员可以使用以下代码完成这些订单:
                <div className="order-address">
                    <div>Id</div>
                    <div className="second-column" title={order.id}>{order.id}</div>
                    <div>Buyer</div>
                    <div className="second-column" title={order.buyer}>{order.buyer}</div>
                    <div>Name and surname</div>
                    <div className="second-column" title={order.nameSurname}>{order.nameSurname}</div>
                    <div>Line 1 direction</div>
                    <div className="second-column" title={order.lineOneDirection}>{order.lineOneDirection}</div>
                    <div>Line 2 direction</div>
                    <div className="second-column" title={order.lineTwoDirection}>{order.lineTwoDirection}</div>
                    <div>City</div>
                    <div className="second-column" title={order.city}>{order.city}</div>
                    <div>State or region</div>
                    <div className="second-column" title={order.stateRegion}>{order.stateRegion}</div>
                    <div>Postal code</div>
                    <div className="second-column">{order.postalCode}</div>
                    <div>Country</div>
                    <div className="second-column" title={order.country}>{order.country}</div>
                    <div>Phone</div>
                    <div className="second-column">{order.phone}</div>
                    <div>State</div>
                    <div className="second-column" title={order.state}>{order.state}</div>
                </div>
            </div>
        )
    })
  1. 我们对未决的买方订单做同样的事情:我们首先显示产品数据,使用下面的代码:
 await this.state.pendingBuyerOrders.asyncForEach(order => {
        pendingBuyerOrdersHtml.push(
            <div key={order.id} className="product">
                <img className="product-image" src={order.image} />
                <div className="product-data">
                    <h3 className="product-title">{order.title}</h3>
                    <div className="product-state">State: {order.state}</div>
                    <div className="product-description">{order.description.substring(0, 15) + '...'}</div>
                    <div className="product-price">{order.price} ETH</div>
                    <button onClick={() => {
                        this.props.setState({product: order})
                        this.props.redirectTo('/product')
                    }} className="product-view" type="button">View</button>
                </div>
  1. 地址数据将完全相同,因此将其复制并粘贴到这个未决买方订单循环中。我们使用相同的代码,因为我们需要更新每个 HTML 块的外观,但是类名必须不同。使用以下代码将for循环添加到已完成订单数组中:
    await this.state.completedOrders.asyncForEach(order => {
        completedOrdersHtml.push(
            <div key={order.id} className="product">
                <img className="product-image" src={order.image} />
                <div className="product-data">
                    <h3 className="product-title">{order.title}</h3>
                    <div className="product-state">State: {order.state}</div>
                    <div className="product-description">{order.description.substring(0, 15) + '...'}</div>
                    <div className="product-price">{order.price} ETH</div>
                    <button onClick={() => {
                        this.props.setState({product: order})
                        this.props.redirectTo('/product')
                    }} className="product-view" type="button">View</button>
                </div>
  1. 将地址块粘贴在产品数据的正下方。用setState()方法更新该组件的状态:
    this.setState({pendingSellerOrdersHtml, pendingBuyerOrdersHtml, completedOrdersHtml})

这是一个很大的功能,因为为了保持简单,我们有重复的功能。对于三个订单数组,我们有三个循环,这样我们可以将订单信息交给用户处理。没有什么太花哨,只是在一个干净的设计数据。我们将数据添加到state对象中,这样我们就可以轻松地显示它。

  1. 创建一个setup()函数,在组件加载时运行这两个函数,如下面的代码所示:
bytes32(name) {
    return myWeb3.utils.fromAscii(name)
}

async setup() {
    await this.getOrders(5)
    await this.displayOrders()
}
  1. 在这种情况下,我们要求每种类型五个订单,因为我们不想让用户不知所措——这很容易根据您的偏好进行更改。您甚至可以在 UI 中添加一个滑块,以便用户可以更改显示的项目数量。render()函数也被更新以反映买方的地址数据,如以下代码所示:
render() {
    return (
        <div>
            <Header />
            <div className="orders-page">
                <div>
                    <h3 className="order-title">PENDING ORDERS AS A SELLER</h3>
                    {this.state.pendingSellerOrdersHtml}
                </div>

                <div>
                    <h3 className="order-title">PENDING ORDERS AS A BUYER</h3>
                    {this.state.pendingBuyerOrdersHtml}
                </div>

                <div className="completed-orders-container">
                    <h3 className="order-title">COMPLETED ORDERS</h3>
                    {this.state.completedOrdersHtml}
                </div>
            </div>
        </div>
    )
}

这是对Orders组件的一整套修改。请看一下 GitHub 官方链接中的更新实现:https://GitHub . com/merlox/ecommerce-dapp/blob/master/src/components/orders . js

你可以在https://github . com/merlox/ecommerce-dapp/blob/master/src/index . styl找到更新的 CSS 代码,在那里你会得到完全相同的设计。

那就是给你的整个电商 dApp!下面是它的外观,这样您就可以看到这个简单而强大的应用程序的潜力:

记住将您的智能合约部署到ropsten并运行npm run dev来启动 webpack 服务器,以便您可以与它进行交互。这是电子商务部以太坊能做什么的原型;既然您已经理解了智能合约如何与用户界面交互,那么现在就看您自己了。

请务必在 GitHub 链接上查看本章代码,网址:https://github.com/merlox/ecommerce-dapp

摘要

在本章中,您首先了解了使用 ERC-721 令牌创建独特产品市场的潜力,该市场使用分散智能合约技术,以便您可以轻松管理用户自由创建的 NFT。然后,您构建了一个清晰的界面来显示最重要的数据,以便用户有一个舒适的地方来与底层智能合约进行交互。接下来,您学习了 NFT 令牌的工作原理,包括它们的所有功能,从而构建了智能合约。您部署了自己版本的 ERC-721 标准,然后创建了电子商务智能合约,该合约包含向公众发布产品所需的逻辑,以便其他人可以使用真正的以太坊购买产品。最后,通过创建必要的函数来与 React 用户界面上的智能契约进行交互,从而将所有东西放在一起。

在下一章中,我们将更进一步,构建一个去中心化的银行和贷款平台,实现复杂的智能合约系统,以保证人们能够获得安全的资金储备,并有一个用户界面供他们与之交互。


我们一直在努力

apachecn/AiLearning

【布客】中文翻译组