阅读本文前,你应该对以太坊、智能合约有所了解,如果你还不了解,建议你先看以太坊是什么
除此之外,你最好还了解一些react知识。
本文通过实例教大家来开发去中心化应用,应用效果如图:
从本文,你可以学习到:
项目背景
Pet有一个宠物店,有16只宠物,他想开发一个去中心化应用,让大家来领养宠物。
在truffle box中,已经提供了pet-shop的网站部分的代码和react框架,我们只需要编写合约及交互部分。
环境搭建
创建项目
mkdir pet-shop-react
cd pet-shop-react
> truffle unbox react-box
Downloading...
Unpacking...
Setting up...
Unbox successful. Sweet!
Commands:
Compile: truffle compile
Migrate: truffle migrate
Test contracts: truffle test
Test dapp: npm test
Run dev server: npm run start
Build for production: npm run build
也可以使用truffle init 来创建一个全新的项目。
项目目录结构
contracts/ 智能合约的文件夹,所有的智能合约文件都放置在这里,里面包含一个重要的合约Migrations.sol(稍后再讲)
migrations/ 用来处理部署(迁移)智能合约 ,迁移是一个额外特别的合约用来保存合约的变化。
test/ 智能合约测试用例文件夹
truffle.js/ 配置文件
src/ 前端业务文件夹
build/ 合约编译后存放文件夹
编写智能合约
智能合约承担着分布式应用的后台逻辑和存储。智能合约使用solidity编写,可阅读
solidity系列文章
在contracts目录下,添加合约文件Adoption.sol
pragma solidity ^0.4.17;
contract Adoption {
address[16] public adopters; // 保存领养者的地址
// 领养宠物
function adopt(uint petId) public returns (uint) {
require(petId >= 0 && petId <= 15); // 确保id在数组长度内
adopters[petId] = msg.sender; // 保存调用这地址
return petId;
}
// 返回领养者
function getAdopters() public view returns (address[16]) {
return adopters;
}
}
智能合约
Truffle集成了一个开发者控制台,可用来生成一个开发链用来测试和部署智能合约。
编译
Solidity是编译型语言,需要把可读的Solidity代码编译为EVM字节码才能运行。
> truffle compile
Compiling ./contracts/Adoption.sol...
Compiling ./contracts/Migrations.sol...
Compiling ./contracts/SimpleStorage.sol...
Writing artifacts to ./build/contracts
部署
编译之后,就可以部署到区块链上。
在migrations文件夹下已经有一个1_initial_migration.js部署脚本,用来部署Migrations.sol合约。
Migrations.sol 用来确保不会部署相同的合约。
现在我们来创建一个自己的部署脚本3_deploy_contracts.js
var Adoption = artifacts.require("./Adoption.sol");
module.exports = function(deployer) {
deployer.deploy(Adoption);
};
启动Ganache:
修改truffle.js 文件:
module.exports = {
// See
networks: {
development: {
host: "localhost",
port: 7545,
network_id: "*" // Match any network id
}
}
};
部署
truffle migrate
Using network 'development'.
Running migration: 1_initial_migration.js
Replacing Migrations...
... 0xb7a9652d5fd73a7df1703c874e4fc510f5b714fb61203ada3931babfd7f3cb25
Migrations: 0x8cdaf0cd259887258bc13a92c0a6da92698644c0
Saving successful migration to network...
... 0xd7bc86d31bee32fa3988f1c1eabce403a1b5d570340a3a9cdba53a472ee8c956
Saving artifacts...
Running migration: 2_deploy_contracts.js
Replacing SimpleStorage...
... 0x989a1bf7b17292d05bc5fbc5a70d27d2b002ba2d0265d366f836f619763a733e
SimpleStorage: 0x345ca3e014aaf5dca488057592ee47305d9b3e10
Saving successful migration to network...
... 0xf36163615f41ef7ed8f4a8f192149a0bf633fe1a2398ce001bf44c43dc7bdda0
Saving artifacts...
Running migration: 3_deploy_contracts.js
Deploying Adoption...
... 0xda3a44984cadaf6481a4b462c495b6dbdc3ab77592de062ac3d286c24e6fd1ec
Adoption: 0x8f0483125fcb9aaaefa9209d8e9d7b9c8b9fb90f
Saving successful migration to network...
... 0x55b13865c62964c3b211dd22c1106101855e698e00dbf6d21aad05d6707cf626
Saving artifacts...
到上面为止服务器端就开发完成。
React编写页面
在src目录下,创建components和images文件夹
cd src
mkdir components
mkdir images
在util编写公共方法
getTransactionReceiptMined.js:
module.exports = function getTransactionReceiptMined(txHash, interval) {
const self = this;
const transactionReceiptAsync = function(resolve, reject) {
self.getTransactionReceipt(txHash, (error, receipt) => {
if (error) {
reject(error);
} else if (receipt == null) {
setTimeout(
() => transactionReceiptAsync(resolve, reject),
interval ? interval : 500);
} else {
resolve(receipt);
}
});
};
if (Array.isArray(txHash)) {
return Promise.all(txHash.map(
oneTxHash => self.getTransactionReceiptMined(oneTxHash, interval)));
} else if (typeof txHash === "string") {
return new Promise(transactionReceiptAsync);
} else {
throw new Error("Invalid Type: " + txHash);
}
};
isAdopted.js:
export default function isAdopted(adopter) {
return adopter !== '0x0000000000000000000000000000000000000000';
}
将官方的pet shop中image下图片拷贝到image目录
创建dogImageMap.js文件:
import boxer from './images/boxer.jpeg';
import frenchBulldog from './images/french-bulldog.jpeg';
import goldenRetriever from './images/golden-retriever.jpeg';
import scottishTerrier from './images/scottish-terrier.jpeg';
export default function dogImageMap(path) {
switch(path) {
case 'images/boxer.jpeg': {
return boxer;
}
case 'images/french-bulldog.jpeg': {
return frenchBulldog;
}
case 'images/golden-retriever.jpeg': {
return goldenRetriever;
}
default: {
return scottishTerrier;
}
}
}
在compents文件下创建Pet.js
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import dogImageMap from '../dogImageMap';
import isAdopted from '../utils/isAdopted';
class Pet extends Component {
constructor(props) {
super(props);
this.state = {
pending: false,
}
}
render() {
const {
pet,
adopter,
index,
} = this.props;
return (
Breed: {pet.breed}
Age: {pet.age}
Location: {pet.location}
Owner: {(adopter)}
);
}
}
Pet.propTypes = {
pet: PropTypes.object,
adopters: PropTypes.string,
index: PropTypes.number.isRequired,
adoptPet: PropTypes.func.isRequired,
}
export default Pet;
创建pets.js:
export const petsArray = [
{
"id": 0,
"name": "Frieda",
"picture": "images/scottish-terrier.jpeg",
"age": 3,
"breed": "Scottish Terrier",
"location": "Lisco, Alabama"
},
{
"id": 1,
"name": "Gina",
"picture": "images/scottish-terrier.jpeg",
"age": 3,
"breed": "Scottish Terrier",
"location": "Tooleville, West Virginia"
},
{
"id": 2,
"name": "Collins",
"picture": "images/french-bulldog.jpeg",
"age": 2,
"breed": "French Bulldog",
"location": "Freeburn, Idaho"
},
{
"id": 3,
"name": "Melissa",
"picture": "images/boxer.jpeg",
"age": 2,
"breed": "Boxer",
"location": "Camas, Pennsylvania"
},
{
"id": 4,
"name": "Jeanine",
"picture": "images/french-bulldog.jpeg",
"age": 2,
"breed": "French Bulldog",
"location": "Gerber, South Dakota"
},
{
"id": 5,
"name": "Elvia",
"picture": "images/french-bulldog.jpeg",
"age": 3,
"breed": "French Bulldog",
"location": "Innsbrook, Illinois"
},
{
"id": 6,
"name": "Latisha",
"picture": "images/golden-retriever.jpeg",
"age": 3,
"breed": "Golden Retriever",
"location": "Soudan, Louisiana"
},
{
"id": 7,
"name": "Coleman",
"picture": "images/golden-retriever.jpeg",
"age": 3,
"breed": "Golden Retriever",
"location": "Jacksonwald, Palau"
},
{
"id": 8,
"name": "Nichole",
"picture": "images/french-bulldog.jpeg",
"age": 2,
"breed": "French Bulldog",
"location": "Honolulu, Hawaii"
},
{
"id": 9,
"name": "Fran",
"picture": "images/boxer.jpeg",
"age": 3,
"breed": "Boxer",
"location": "Matheny, Utah"
},
{
"id": 10,
"name": "Leonor",
"picture": "images/boxer.jpeg",
"age": 2,
"breed": "Boxer",
"location": "Tyhee, Indiana"
},
{
"id": 11,
"name": "Dean",
"picture": "images/scottish-terrier.jpeg",
"age": 3,
"breed": "Scottish Terrier",
"location": "Windsor, Montana"
},
{
"id": 12,
"name": "Stevenson",
"picture": "images/french-bulldog.jpeg",
"age": 3,
"breed": "French Bulldog",
"location": "Kingstowne, Nevada"
},
{
"id": 13,
"name": "Kristina",
"picture": "images/golden-retriever.jpeg",
"age": 4,
"breed": "Golden Retriever",
"location": "Sultana, Massachusetts"
},
{
"id": 14,
"name": "Ethel",
"picture": "images/golden-retriever.jpeg",
"age": 2,
"breed": "Golden Retriever",
"location": "Broadlands, Oregon"
},
{
"id": 15,
"name": "Terry",
"picture": "images/golden-retriever.jpeg",
"age": 2,
"breed": "Golden Retriever",
"location": "Dawn, Wisconsin"
}
]
修改app.js文件:
import React from 'react'
import _ from 'lodash';
import AdoptionContract from '../build/contracts/Adoption.json'
import { petsArray } from './pets.js';
import getWeb3 from './utils/getWeb3'
import Pet from './components/Pet';
import './css/oswald.css'
import './css/open-sans.css'
import './css/pure-min.css'
import './App.css'
class App extends React.Component {
constructor(props) {
super(props)
this.state = {
web3: {},
adoptionContract: null,
pets: [],
adopters: [],
blockNumber: null,
}
this.adoptPet = this.adoptPet.bind(this);
this.getBlockNumber = this.getBlockNumber.bind(this);
}
componentWillMount() {
getWeb3
.then(results => {
this.setState({
web3: results.web3
}, () => {
this.getBlockNumber()
this.instantiateContract()
this.fetchPets();
});
})
.catch(() => {
console.log('Error finding web3.')
})
}
getBlockNumber() {
this.state.web3.eth.getBlockNumber((error, block) => {
this.setState({ blockNumber: block });
});
}
fetchPets() {
this.setState({ pets: petsArray });
}
adoptPet(item) {
const { adoptionContract } = this.state;
const newAdopters = this.state.adopters;
this.state.web3.eth.getAccounts((error, accounts) => {
adoptionContract.deployed().then((instance) => {
return instance.adopt(item, { from: accounts[0]})
.then((result) => {
this.state.web3.eth.getTransactionReceiptMined(result.tx)
.then((newResult) => {
newAdopters[item] = accounts[0];
this.setState({
blockNumber: newResult.blockNumber,
adopters: newAdopters
})
});
})
.catch((err) => {
console.log(err.message);
})
});
});
}
instantiateContract() {
const contract = require('truffle-contract')
const adoption = contract(AdoptionContract);
adoption.setProvider(this.state.web3.currentProvider)
adoption.deployed().then((instance) => {
return instance.getAdopters.call().then((result) => {
this.setState({
adopters: result,
adoptionContract: adoption
});
});
});
}
render() {
const {
pets,
adopters
} = this.state
return (
{
_.map(pets, (pet, index) => {
return (
pet={pet} adopter={adopters[index]} index={index} adoptPet={this.adoptPet} />
)
})
}
);
}
}
export default App
修改utils下getWeb3.js文件到url路径到7454端口。
import Web3 from 'web3'
import getTransactionReceiptMined from './getTransactionReceiptMined';
let getWeb3 = new Promise(function(resolve, reject) {
// Wait for loading completion to avoid race conditions with web3 injection timing.
window.addEventListener('load', function() {
var results
var web3 = window.web3
// Checking if Web3 has been injected by the browser (Mist/MetaMask)
if (typeof web3 !== 'undefined') {
// Use Mist/MetaMask's provider.
web3 = new Web3(web3.currentProvider)
web3.eth.getTransactionReceiptMined = getTransactionReceiptMined;
results = {
web3: web3
}
console.log('Injected web3 detected.');
resolve(results)
} else {
// Fallback to localhost if no web3 injection. We've configured this to
// use the development console's port by default.
var provider = new Web3.providers.HttpProvider('http://127.0.0.1:7545')
web3 = new Web3(provider)
results = {
web3: web3
}
console.log('No web3 instance injected, using Local web3.');
resolve(results)
}
})
})
export default getWeb3
由于copy官方例子,使用了bootstrap.mini.css
将css放入到public文件夹中,并修改index.html,加上下面这句话:
运行:
yarn run start
在浏览器中运行
安装 MetaMask
MetaMask 是一款插件形式的以太坊轻客户端,开发过程中使用MetaMask和我们的dapp进行交互是个很好的选择,通过此链接安装,安装完成后,浏览器工具条会显示一个小狐狸图标。
配置钱包
在接受隐私说明后,会出现页面如下:
这里我们通过还原一个Ganache为我们创建好的钱包,作为我们的开发测试钱包。点击页面的Import Existing DEN,输入Ganache显示的助记词。
candy maple cake sugar pudding cream honey rich smooth crumble sweet treat |
然后自己想要的密码,点击OK。
如图:
连接开发区块链网络
默认连接的是以太坊主网(左上角显示),选择Custom RPC,添加一个网络:http://127.0.0.1:7545,点返回后,显示如下:
这是左上角显示为Private Network,账号是Ganache中默认的第一个账号。
注意 在交易时候 请注意保持metamask保持活跃状态。
点击领取。
成功:
感谢Tiny熊和kkomaz,pet shop 相关文章。https://blog.csdn.net/yuanfangyuan_block/article/details/79412830