-
如何从零开始创建一个智能合约
-
如何在以太坊
Ropsten
测试网络上部署一个智能合约 -
如何使用
Remix
调试工具部署并调试Solidity
智能合约 -
如何创建一个前端Dapp应用,并用
webpack
打包 -
如何将Dapp与已部署好的智能合约连接
-
如何在IPFS网络上部署Dapp应用
该Dapp完成后,在浏览器中运行,并结合Metamask
钱包使用。
-
区块链:将在以太坊测试网络
Ropsten
上运行 -
存储:将在分布式存储
IPFS
网络上永远保存该应用 -
前端:使用
React
开发,webpack
打包 -
智能合约语言:使用
Solidity 0.4.11
-
智能合约调试与部署:使用在线
Remix
-
前端智能合约连接:使用
web3.js
-
智能合约开发框架:使用
Truffle
编译、测试并部署智能合约(辅助) -
开发环境:
Nodejs
最新版 -
钱包:
Metamask
浏览器钱包
一、配置项目
二、智能合约编程
三、创建前端应用
四、使用IPFS部署应用
确保系统内已经安装了Nodejs
创建一个新的目录:casino-ethereum
,然后执行:
//安装truffle,并加入到devDependencies (-D) and globally (-g)
$ npm install -D -g truffle
//启动truffle,下载truffle框架
$ truffle init
//生成package.json包
$ npm init -y
//安装需要的模块,包括:webpack,react,babel,css以、json以及以太坊前端工具web3,这些也是开发以太坊前端应用的基本工具。注意:不要安装1.0.0的beta版
$ npm install -D webpack react react-dom babel-core babel-loader babel-preset-react babel-preset-env css-loader style-loader json-loader [email protected]
//安装http-server服务,以便通过localhost:3030端口访问,需要全局安装
npm i -g http-server
//安装babel-loader的预处理模块
npm i -D babel-preset-stage-2
npm i -D babel-preset-es2015
在目录下,新建文件webpack.config.js
,这是打包配置文件,输入以下代码:
const path = require('path')
module.exports = {
mode: 'development', //3.0版本后必须增加,生产环境是换成 production;之前webpack版本,不要使用mode
entry: path.join(__dirname, 'src/js', 'index.js'), // 所有前端代码在 src/js/index.js 中
output: {
path: path.join(__dirname, 'dist'),
filename: 'build.js' // 最终程序文件在 dist/build.js
},
module: {
rules: [{ // webpack 3.0版本后,使用rules;之前版本用 loaders
test: /\.css$/, // 在 react 中加载 css
use: ['style-loader', 'css-loader'], // 加载 css 时使用的工具
include: /src/
}, {
test: /\.jsx?$/, // 加载 jsx 文件,jsx 是javascript的一种糖果文件
loader: 'babel-loader', // 加载器
exclude: /node_modules/,
query: {
presets: ['es2015', 'react', 'stage-2']
}
}, {
test: /\.json$/, // json 文件加载
exclude: /node_modules/,
loader: 'json-loader'
}]
}
}
然后,创建如下目录:
-
目录:
src/js
,并在该目录下创建index.js
文件 -
目录:
src/css
,并在该目录下创建index.css
文件 -
目录:
build
,并在该目录下创建index.html
文件
最终的目录结构为:
contracts/
-- Migrations.sol
migrations/
node_modules/
test/
src/
-- css/index.css
-- js/index.js
build/
-- index.html
package.json
truffle-config.js
truffle.js
webpack.config.js
将以下代码粘贴至:build/index.html
文件中:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>以太坊开发 案例二:猜数游戏</title>
</head>
<body>
<div id="root"></div>
<script src="build.js"></script>
</body>
</html>
以上代码中,设定了id为root
的<div>
,用于插入React生成的代码。
另外,webpack
生成的编译文件,放在build.js
中。
在本章中,将从零开始讲述智能合约编写,并在正式部署前调试成功。
首先,在contracts
目录下添加文件:Casino.sol
。
在文件头添加Solidity版本声明和程序框架:
pragma solidity ^0.4.20;
contract Casino { //只有两种类型:contract和library。类名必须要与文件名完全一致
address public owner; //定义owner变量
function Casino() public {
owner = msg.sender; //构造函数只在智能合约部署时一次性运行,将运行该合约的账户设为合约所有人owner
}
function kill() public {
if(msg.sender == owner) selfdestruct(owner); //如果是合约所有人发送的kill命令,则将此合约销毁。合约销毁后,该合约中的所有代币将自动转入Owner账户。此操作只在被黑客攻击导致无法挽回时使用,但是建议每个合约都需要部署该方法。
}
}
结合该项目的目的,我们需要考虑以下事情:
-
记录有多少用户已经下注,以及每个用户下注的数字
-
每注的最小下注金额(ETH)
-
总的下注金额(ETH)
-
一个记录总下注数的变量
-
合适结束下注,并开奖
-
将奖金扣除费用后,自动发放给每位中奖者
-
如果没有中奖者,则将奖金平均后返还每位参与者
下面我们将逐一讲述:
首先,创建一个玩家的struct
数据类型,然后通过mapping
类型定义玩家数组:
struct Player {
uint256 amountBet; //下注金额
uint256 numberSelected; //选择的数字
}
mapping(address => Player) public playerInfo; //实例化playerInfo
以后,可以使用playerInfo[用户的以太坊地址].amountBet
来获取或设置该用户的下注金额。
然后,定义一些公共变量:
uint256 public minimumBet = 100 finney;//最小下注金额,0.1ETH
uint256 public totalBet; //总下注金额
uint256 public numberOfBets; //已下注人数
uint256 public maxAmountOfBets = 100; //最多下注人数
address[] public players; //玩家数组
这里再加两种常用的数据类型:byte32
和string
,这两种都是字符串,需要加双引号。
我们修改一下之前的构造函数,让初始化合约时,传入一个最小下注金额的常量:
function Casino(uint256 _minimumBet){
owner = msg.sender;
if(_minimumBet > 0 ) minimumBet = _minimumBet;
}
注意:在Solidity
中,代币的单位统一为wei
,不存在小数。一个ETH=1000...0 wei(一共有18个0),建议使用换算计算器
接下去,添加以下代码:
function bet(uint256 numberSelected) public payable {
require(!checkPlayerExists(msg.sender)); //判断发送指令的人是否已在玩家列表中登记
require(numberSelected >= 1 && numberSelected <= 10); //判断选择的数字范围
require(msg.value >= minimumBet); //判断玩家下注的金额是否满足最低下注额
playerInfo[msg.sender].amountBet = msg.value; //记录下注金额到数组
playerInfo[msg.sender].numberSelected = numberSelected; //记录下注的数字到数组
numberOfBets++; //总下注次数+1
players.push(msg.sender); //把用户帐号压入player数组
totalBet += msg.value; //总下注金额增加
if(numberOfBets >= maxAmountOfBets) generateNumberWinner(); //判断下注的人次是否超过了100个
}
该代码是用户下注时执行,其中:
-
msg.sender
是发送者帐号,msg.value
是发送者金额。 -
payable
是一个modifier
,函数修改器,用在智能合约中进行一些函数功能行为的修改,例如对函数执行前置条件的自动检查,有点像条件函数,或者事件触发器。
在这里,所有涉及支付的函数,都需要设定为payable
,如果没有该modifier
函数,打到合约的代币会被自动退回。
常见的modifier
函数还有onlyOwner
,表示只有owner才能执行该函数,代码如下:
modifier onlyOwner {
require(msg.sender == owner) _;
}
-
require()
函数用来判断括号中的条件,如果是True
,则继续,否则,将用户的代币返回给用户账户,函数结束。与0.4.10版本前Solidity
使用if...throw
一样,但代码更整洁。类似的还有assert()
,只是assert()
对于错误的操作会扣除Gas,require()
不会。 -
checkPlayerExists
函数用来判断用户是否存在,代码如下:
function checkPlayerExists(address player) public constant returns(bool){
for(uint256 i = 0; i < players.length; i++){
if(players[i] == player) return true;
}
return false;
}
以上函数中的返回值类型为constant
,是指直接返回某个值,而无需改变账户状态,这个操作无需消耗Gas。
一般constant
会和returns()
返回值定义,一起使用。
- 上面代码的最后一句,判断下注人数如果超过100,则执行开奖函数
generateNumberWinner
:
function generateNumberWinner() public {
uint256 numberGenerated = block.number % 10 + 1;
distributePrizes(numberGenerated); //执行奖金分发函数
}
这里使用了不安全的随机数获取方法,即当前块的高度block.number
,并取其个位数加1,该方法只能用于教学,不能用于实际使用。因为很容易被猜到。
本教程原作者已经在完整版中,将Oraclize工具生成真实的随机数,点击这里获取完整代码。
- 实现奖金分发函数
function distributePrizes(uint256 numberWinner) public {
address[100] memory winners; //保存获胜者的临时数组,memory类型的数据在函数执行完毕后释放,这类临时变量必须是固定长度,本例中定义长度为100,即最多全部猜中情况下,获胜者不会超过100名
uint256 hasWiner = 0; //用来判断是否有赢家
uint256 count = 0; // 因为winners数组指定长度,所以无法通过length获取获奖人数,只能设这个变量
for (uint256 i = 0; i < players.length; i++) {
address playerAddress = players[i];
if (playerInfo[playerAddress].numberSelected == numberWinner) {
//如果玩家选的数字等于随机生成的获奖数字,则将获奖者压入到数组
winners[count] = playerAddress;
count++;
}
delete playerInfo[playerAddress]; // 无论是否获奖,都将释放玩家数据,不做保存
}
if (count == 0) {
//如果没有人猜中
count = maxAmountOfBets;
hasWiner = 0;
} else {
hasWiner = 1;
}
uint256 winnerEtherAmount = totalBet * 95 / 100 / count; // 系统抽取5%
for (uint256 j = 0; j < count; j++) {
if (hasWiner == 1) {
//如果有人猜中,则分给猜中的人
if (winners[j] != address(0)) winners[j].transfer(winnerEtherAmount);
} else {
//如果没有人猜中,则分给所有竞猜的人
if (players[j] != address(0)) players[j].transfer(winnerEtherAmount);
}
}
players.length = 0; // Delete all the players array
totalBet = 0;
numberOfBets = 0;
}
在Metamask
钱包的Rospten
测试网络中新建一个账户,在浏览器中打开以太坊水龙头工具https://faucet.metamask.io/,点击绿色按钮,就可以免费获得测试用的以太币。
打开Remix网站,将以上代码复制到Remix
调试工具中,并刷新Remix
网页。
点击右上角Run
标签,注意在Environment
一栏中,选择:Injected Web Rospten
。然后会在Account
一栏中看到Metamask
钱包中的账户名,如下图:
点击右上角Compile
标签,点击Start to Comile
开始编译。如果一切正常,编译通过。
回到Run
标签,点击Deploy
按钮,开始部署合约。正常情况,会跳出Metamask
钱包,输入Gas
费用(注意,千万不要是0),点击Submit
,在console窗口中会提示transaction
的区块链链接地址,过一会,会提示部署成功的信息,然后点击Remix
监控窗口提示的链接,点击打开浏览器,查看部署合约的交易,并获取合约地址。
注意:请务必将合约地址复制到记事本
。
至此合约部署完毕。
在部署过程中,经常会提示错误,不要强行发送transaction
,仔细查看代码,每个很小的bug,都可能会让程序无法正常运行,并提示报错。
每次修改bug,都需要重新部署合约,一个成熟的合约,要不厌其烦的反反复复检查代码、优化代码。
将复制的智能合约地址,复制到at Address
文本框中,点击按钮调用该合约。
使用每个public
函数,以及所有public
变量。
使用javascript部署合约
在migrations
目录下,新建一个文件2_deploy_contracts.js
,内容如下:
var Casino = artifacts.require("./Casino.sol");
module.exports = function(deployer) {
//前面两个参数分别对应合约构造函数中的两个参数,gas为部署合约时的Gas费用
deployer.deploy(web3.toWei(0.1, 'ether'), 100, {gas: 3000000});
};
使用truffle部署智能合约
除了Remix
调试工具之外,Truffle
也有一套部署工具。运行truffle compile
编译源码,编译成功后,在build/contracts/
目录下,会生成Casino.json
文件。
打开src/js/index.js
文件,加入以下代码:
import React from 'react'
import ReactDOM from 'react-dom'
import Web3 from 'web3'
import './../css/index.css'
class App extends React.Component {
constructor(props){
super(props)
this.state = {
lastWinner: 0,
timer: 0
}
}
voteNumber(number){
console.log(number)
}
render(){
return (
<div className="main-container">
<h1>Bet for your best number and win huge amounts of Ether</h1>
<div className="block">
<h4>Timer:</h4>
<span ref="timer"> {this.state.timer}</span>
</div>
<div className="block">
<h4>Last winner:</h4>
<span ref="last-winner">{this.state.lastWinner}</span>
</div>
<hr/>
<h2>Vote for the next number</h2>
<ul>
<li onClick={() => {this.voteNumber(1)}}>1</li>
<li onClick={() => {this.voteNumber(2)}}>2</li>
<li onClick={() => {this.voteNumber(3)}}>3</li>
<li onClick={() => {this.voteNumber(4)}}>4</li>
<li onClick={() => {this.voteNumber(5)}}>5</li>
<li onClick={() => {this.voteNumber(6)}}>6</li>
<li onClick={() => {this.voteNumber(7)}}>7</li>
<li onClick={() => {this.voteNumber(8)}}>8</li>
<li onClick={() => {this.voteNumber(9)}}>9</li>
<li onClick={() => {this.voteNumber(10)}}>10</li>
</ul>
</div>
)
}
}
ReactDOM.render(
<App />,
document.querySelector('#root')
)
打开src/css/index.css
文件,加入以下代码:
body {
font-family: 'open sans';
margin: 0;
}
ul {
list-style-type: none;
padding-left: 0;
display: flex;
}
li {
padding: 40px;
border: 2px solid rgb(30, 134, 255);
margin-right: 5px;
border-radius: 10px;
cursor: pointer;
}
li:hover {
background-color: rgb(30, 134, 255);
color: white;
}
li:active {
opacity: 0.7;
}
* {
color: #444444;
}
.main-container {
padding: 20px;
}
.block {
display: flex;
align-items: center;
}
.number-selected {
background-color: rgb(30, 134, 255);
color: white;
}
.bet-input {
padding: 15px;
border-radius: 10px;
border: 1px solid lightgrey;
font-size: 15pt;
margin: 0 10px;
}
在命令行中,使用命令:webpack
或者npm run build
编译打包。
以上命令在package.json
文件中配置,查看scripts
,如下:
"scripts": {
"build": "webpack --log-level=debug", //npm run build等同于webpack命令
"start": "webpack-dev-server --port 3030 --inline --content-base ./build"
},
然后运行:npm start
启动web服务,默认情况下npm init
时会生成8080
端口的web服务,如果冲突,可以改为其他端口。如本例改为了3030
本地端口。
接着在浏览器中打开: http://127.0.0.1:3030
,可以看到如下网页:
在Remix
中部署合约,找到ABI文件,并复制。
在index.js
中添加如下代码:
if (typeof web3 != "undefined") {
//启动Metamask
console.log("Using web3 detected from external source like Metamask")
this.web3 = new Web3(web3.currentProvider)
} else {
//启用本地以太坊网络或者Truffle的Ganache
this.web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:8545"))
}
const contractAddress = "0xB2bE09289F9f7103964f57aAF04119fcB79d2149" //本案例部署的合约帐号,可换
const abi = 合约的ABI数组,一个非常长的数组
const MyContract = web3.eth.contract(abi)
this.state.ContractInstance = MyContract.at(contractAddress)
执行合约中函数的基本方法,以合约中的bet
函数为例:
yourContractInstance.bet(7, { // 7为函数参数,即下注的数字
gas: 300000, // Gas
from: web3.eth.accounts[0], // 用户帐号,accounts是数组,取第一个元素
value: web3.toWei(0.1, 'ether') // 发送金额,单位wei
}, (err, result) => {...})
如果调用不需要Gas的方法或者变量,使用如下代码:
yourContractInstance.maxAmountOfBets((err, result) => {
if(result != null) {...}
})
在index.js
继续添加以下代码:
componentDidMount() {
this.updateState()
this.setupListeners()
setInterval(this.updateState.bind(this), 10e3)
}
updateState() {
this.state.ContractInstance.minimumBet((err, result) => {
if (result != null) {
this.setState({
minimumBet: parseFloat(web3.fromWei(result, 'ether'))
})
}
})
this.state.ContractInstance.totalBet((err, result) => {
if (result != null) {
this.setState({
totalBet: parseFloat(web3.fromWei(result, 'ether'))
})
}
})
this.state.ContractInstance.numberOfBets((err, result) => {
if (result != null) {
this.setState({
numberOfBets: parseInt(result)
})
}
})
this.state.ContractInstance.maxAmountOfBets((err, result) => {
if (result != null) {
this.setState({
maxAmountOfBets: parseInt(result)
})
}
})
}
// 设置监听器
setupListeners() {
let liNodes = this.refs.numbers.querySelectorAll('li')
liNodes.forEach(number => {
number.addEventListener('click', event => {
event.target.className = 'number-selected'
this.voteNumber(parseInt(event.target.innerHTML), done => {
// Remove the other number selected
for (let i = 0; i < liNodes.length; i++) {
liNodes[i].className = ''
}
})
})
})
}
// 下注
voteNumber(number, cb) {
let bet = this.refs['ether-bet'].value
if (!bet) bet = 0.1 //默认下注为0.1ETH
if (parseFloat(bet) < this.state.minimumBet) {
alert('You must bet more than the minimum')
cb()
} else {
this.state.ContractInstance.bet(number, {
gas: 300000,
from: web3.eth.accounts[0],
value: web3.toWei(bet, 'ether')
}, (err, result) => {
cb()
})
}
}
至此,主要的程序已经完成,webpack
打包编译,运行npm start
,然后在浏览器中打开:http://127.0.0.1:3030
可以试玩。
在本章,我们将看到IPFS的强大,她可以方便的部署一个去中心化的应用。
启动IPFS网络后,运行以下命令:
ipfs add -r dist/
ipfs name publish 上面生成的Hash值
之后就可以直接用:http://网关/ipfs/网站Hash
进行访问。
比如本教程示例在:http://eternum.io/ipfs/QmfZrt27ohzMZfd6jjXxhvze6ZaLo9e8keRsoTBsdEjfcY
即可在IPFS网络上访问并运行该分布式应用。
好了,本教程到此结束,感谢该教程的原作者Merunas,感谢Satoshi、感谢Vitalic、感谢Daniel,让我们能够在一个完全去中心化的环境中如此方便的开发竞猜类应用:)
1- 教程原文
2- 完整的智能合约源码
3- 以太坊web3中文文档
5- 以太坊单位换算器
7- Solidity文档
8- Web3.js文档