创建基于区块链的电子商务市场
一个分散的电子商务市场是区块链技术的最佳案例之一,原因很简单,你不必支付费用,也不必将你的数据委托给有实力的公司,这些公司会出售这些数据以获取利润。以太坊是一个很好的解决方案,新的 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.html
或index.ejs
- 根据您的喜好选择
index.css
或index.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
文件,导入需要的库,用一些占位符产品用假数据设置初始状态,只是为了看看最终设计会是什么样子。我们通过以下步骤来实现这一点:
- 导入所需的库。我们需要来自
react-router
库中的几个组件,如下面的代码所示:
import React from 'react'
import ReactDOM from 'react-dom'
import MyWeb3 from 'web3'
import { BrowserRouter, Route, withRouter } from 'react-router-dom'
- 使用一些产品创建构造函数,这些产品具有向用户显示尽可能多的信息的必要属性,如下面的代码所示。诸如
title
、description
、id
和price
的属性是必须的:
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: {},
}
}
- 您可以通过复制
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>
)
}
}
- 使用 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)
- 从 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
并看到您的设置了路由器的页面。
创建标题组件
我们的应用程序将有几个买家,卖家和订单页面。这就是为什么尽可能将每个组件分成唯一的块非常重要,这些块可以通过执行以下步骤导入到需要的地方:
- 在
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
- 用
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'
- 用组件实例更新您的
render()
函数,如下面的代码所示:
render() {
return (
<div>
<Route path="/" exact render={() => (
<Header />
)} />
</div>
)
}
您将看到无需刷新 webpack 服务即可自动加载您的标题,如以下屏幕截图所示:
- 现在还不好看,还是用一些
stylus
CSS 改进一下设计吧。如果您还没有配置它,用下面的命令安装stylus
和stylus-loader
库:
npm i -S stylus stylus-loader
- 按如下方式更新您的
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
- 注意顶部的
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 中必须进行的更改:
- 在删除
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'
- 为了简单起见,我为
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()
})
}
- 在您的构造函数中,包含一个
setup()
函数调用,如下面的代码片段所示:
constructor(props) {
super(props)
// State object omitted for simplicity
this.setup()
}
- 用代码实现
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()
}
- 我们已经包含了对
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})
}
- 修改
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 按钮时创建产品页面,这样用户就可以看到关于这个特定产品的更多详细信息。用户将能够在产品页面内购买产品。让我们来完成以下步骤:
- 使用以下代码在组件中添加一个新的
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
- 我们需要一个新的 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>
)
}
}
- 假设我们设置了所需的历史功能,那么当您单击“查看”按钮时,您应该能够访问定制产品页面。当用户单击 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
- 您可以打开 dApp,单击产品的“查看”按钮,查看详细的产品页面,其中显示了大图和完整描述,如以下屏幕截图所示:
剩下的就是添加购买、销售和订单页面。下面是我们如何使用Buy
组件,当用户点击位于产品页面的Buy
按钮时,它将会显示出来:
- 使用以下命令导入所需的库:
import React, { Component } from 'react'
import Header from './Header'
- 用空的状态变量定义
Buy
组件中的构造函数,这样你就知道哪些变量将在整个组件中使用,你可以通过使用下面的代码来实现:
class Buy extends Component {
constructor() {
super()
this.state = {
nameSurname: '',
lineOneDirection: '',
lineTwoDirection: '',
city: '',
stateRegion: '',
postalCode: '',
country: '',
phone: '',
}
}
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>
- 为用户提供一个包含送货信息的块,以包含他们的地址,这样他们就可以免费收到产品,如下面的代码所示:
<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>
- 导出组件,以便将其导入路由器管理器,如以下代码所示:
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'
然后将新的Route
和props
参数添加到刚刚导入到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 代码使其看起来更好:
- 使用以下代码为
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
- 添加
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
组件:
- 导入必要的库来创建 React 组件并包含
Header:
import React from 'react'
import Header from './Header'
- 使用一个空的构造函数创建
Sell
类,该构造函数包含带有用户将要销售的产品的title
、description
、image
和price
的state
对象,如下面的代码所示:
class Sell extends React.Component {
constructor() {
super()
this.state = {
title: '',
description: '',
price: '',
image: '',
}
}
}
- 用简洁的形式创建
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>
)
}
- 使用以下代码导出这个新组件,以便其他文件可以导入它:
export default Sell
保存Sell
组件后,将其导入到您的索引 JavaScript 文件中。我们必须添加一个名为publishProduct
的函数,它将调用相应的智能合约函数。
以下步骤显示了导入此Sell
组件所需的对索引文件(为清晰起见突出显示)的更改:
- 在
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'
- 将
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>
)
}
}
- 添加一些 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 来练习你的技能,从而完成设计。你会发现这比预期的要花更多的时间,但是这是值得的:
- 导入所需的库,如以下代码所示:
import React, { Component } from 'react'
import Header from './Header'
- 使用一些虚构的顺序来定义构造函数,以便您可以看到它的外观,如下面的代码所示:
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()
}
- 我们需要一个函数,通过从智能合同中获取数据来获取用户的订单,同时还将订单标记为已完成。我们还不会实现这些功能,因为我们必须首先创建智能合约,如以下代码所示:
async getUserOrders() {}
async markAsCompleted(product) {}
- 添加这些空函数,然后创建一个名为
displayOrders()
的函数,它将获取状态数据以输出结果 HTML。首先定义内部使用的数组,如下面的代码所示:
async displayOrders() {
let pendingSellOrdersHtml = []
let pendingBuyOrdersHtml = []
let completedSellOrdersHtml = []
let completedBuyOrdersHtml = []
}
- 读取不同的 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>
)
- 如果销售订单的状态是 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>
)
}
})
- 对
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)
})
- 用生成的 HTML 对象更新组件的状态,如下面的代码所示:
this.setState({pendingSellOrdersHtml, pendingBuyOrdersHtml, completedSellOrdersHtml, completedBuyOrdersHtml})
- 创建
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>
)
}
}
- 导出
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
函数的,因为它通过允许将transferFrom
和safeTransferFrom
函数用作正常传输或批准的传输来简化过程,从而消除了对标准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 来从用户界面的智能契约中提取数据。
让我们开始创建电子商务合同,它将所有的市场逻辑放在同一个文件中,因为它不会占用太多空间:
- 定义智能协定所需的变量,从您将需要的结构开始,如下面的代码所示:
/// @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'
}
- 添加映射、数组、变量和构造函数,如以下代码所示:
// 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;
}
}
我们必须首先从结构开始设置变量,在本例中是Product
和Order
。每个订单将通过 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.js
和2_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()
函数,您马上就会看到,这些函数对于从契约中获取数据并正确显示是必要的。
更新索引文件
现在我们有了一个可工作的契约实例,我们可以处理索引文件所需的功能,这样我们就可以将功能包含在较小的组件中,如下面的代码所示:
- 实现
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})
}
- 添加更新后的重定向函数,如以下代码所示:
redirectTo(location) {
this.props.history.push({
pathname: location
})
}
- 通过获取产品的长度并循环每个产品,实现从智能合约中获取产品的函数:
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
函数,我们如何获得所有的产品数据?这个过程是这样的:
- 我们得到产品数组的长度。我们使用
getProductsLength()
函数,因为没有合适的getter
函数,我们无法获得数组的长度。 - 一旦我们知道智能合约中有多少产品可用,我们就循环遍历该大小以运行
products()
函数,该函数可用是因为我们的产品数组是公共的,这意味着它有一个为其自动创建的getter
函数。公共数组必须逐个访问;这就是为什么我们使用一个反向的for
回路。 - 我们需要一个反向循环,先获取最新产品。
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.js
和Product.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:
- 设置所需的数组变量,这在本例中更简单,因为我们要为不同类型的订单创建三个块:
async displayOrders() {
let pendingSellerOrdersHtml = []
let pendingBuyerOrdersHtml = []
let completedOrdersHtml = []
- 如果每种类型的订单都没有订单,我们希望通过使用以下代码显示一条消息,让用户知道没有订单:
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>
))
}
- 通过使用以下代码添加地址部分来更新未决订单:
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>
- 在产品数据的正下方,添加地址信息,以便销售人员可以使用以下代码完成这些订单:
<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>
)
})
- 我们对未决的买方订单做同样的事情:我们首先显示产品数据,使用下面的代码:
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>
- 地址数据将完全相同,因此将其复制并粘贴到这个未决买方订单循环中。我们使用相同的代码,因为我们需要更新每个 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>
- 将地址块粘贴在产品数据的正下方。用
setState()
方法更新该组件的状态:
this.setState({pendingSellerOrdersHtml, pendingBuyerOrdersHtml, completedOrdersHtml})
这是一个很大的功能,因为为了保持简单,我们有重复的功能。对于三个订单数组,我们有三个循环,这样我们可以将订单信息交给用户处理。没有什么太花哨,只是在一个干净的设计数据。我们将数据添加到state
对象中,这样我们就可以轻松地显示它。
- 创建一个
setup()
函数,在组件加载时运行这两个函数,如下面的代码所示:
bytes32(name) {
return myWeb3.utils.fromAscii(name)
}
async setup() {
await this.getOrders(5)
await this.displayOrders()
}
- 在这种情况下,我们要求每种类型五个订单,因为我们不想让用户不知所措——这很容易根据您的偏好进行更改。您甚至可以在 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 用户界面上的智能契约进行交互,从而将所有东西放在一起。
在下一章中,我们将更进一步,构建一个去中心化的银行和贷款平台,实现复杂的智能合约系统,以保证人们能够获得安全的资金储备,并有一个用户界面供他们与之交互。