以太坊彩票项目
目录
项目概述
solidity 编写合约,node.js 编译、部署、获取、交互合约,react搭建前端界面
超详细~
(1)彩票业务规则-智能合约lottery.sol |
---|
[1] 全民参与(play函数) |
[2] 每次投注只能投注1eth |
[3] 每个人可以投多注 |
[4] 仅限管理员可以开奖(KaiJiang函数) |
[5] 仅限管理员可以退奖(TuiJiang函数) |
(2)编译智能合约 01-compile.js |
---|
[1] 导入solc编译器和fs库 |
[2] fs读取contracts文件夹下lottery.sol合约 |
[3] solc编译合约 |
[4] 导出bytecode(机器码)和interface(ABI) |
(3)部署智能合约上链 02-deploy.js |
---|
[1] 获取bytecode和interface |
[2] 导入web3 |
[3] 设置网络,管理员(部署合约的人)实例化web3(.setProvider) |
[4] 拼接合约数据 |
[5] 拼接bytecode |
[6] 返回合约地址(instanceAddress) |
(4)从区块链获取合约实例 |
---|
[1] 获取interface(ABI)和合约地址(instanceAddress) |
[2] 导入web3 |
[3] 并设设置网络,用户(使用者)实例化web3(.setProvider) |
[4] 利用interface(ABI)和合约地址(instanceAddress)获取合约 |
[5] 返回合约,用于交互 |
(5)设计前端,同合约交互 |
---|
[1] 利用react完成界面搭建 |
[2] 在界面中显示返回的数据 |
[3] 待输入 |
1.合约上链交互5部曲示意图
项目具体实现
[1].彩票业务规则示例图
[2].整体项目搭建
系统环境 : MacOS,IDE : Goland2020
- 整体项目结构图(环境搭建完毕图)
- 使用react脚手架
[step-1] 终端进入goland项目文件夹下: cd /XXXXXX/GoProjects/src/
[step-2] 终端安装react应用程序骨架: npm install -g create-react-app
[step-3] 终端创建一个空的项目: create-react-app lottery-react
[step-4] 在goland中打开此项目
-
goland终端里输入npm start启动项目,最终效果如图
-
安装react组件库和配套样式库,并清理工程
[step-1] npm install semantic-ui-react --save
[step-2] npm install semantic-ui-css --save
[step-3] 进⼊src⽬录,保留App.js和index.js,删除掉其他⽂件
[step-4] 删掉App.js和index.js中对删除⽂件的依赖代码
[step-5] 删掉App.js中render函数渲染的内容
[step-6] 手动输入萌新认证:Hello World
[step-7] 在goland中打开此项目
//App.js 清理后代码
import React, {Component} from 'react';
class App extends Component {
render() {
return(
<p>Hello World !</p>
);
}
}
export default App;
//index.js 清理后代码
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
ReactDOM.render(
<App />,
document.getElementById('root')
);
最终效果图
- 安装solc编译器和web3模块,并配置项目
[step-1] npm install solc@0.4.25 --save
[step-2] npm install web3@1.2.11 --save
注意: 若版本不一致,可以通过 name@xxx,指定版本(这两个版本尽量保持一直,否则容易出问题)
[step-3] (我是mac版goland2020,位置同其他版可能不一样,请通过搜索引擎解决)如图配置
[step-4] 新建一个testEnvironment.js 文件,输入最后一幅图,结果如果所示,则配置完成
注意:新建文件后,若显示红色,为git为添加问题,右键文件,找到"Git",选择"+Add";或者上一步中,右下角选择"Always Add"。
- 最后安装的库 package.json
{
"name": "review-lottery",
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.5.0",
"@testing-library/user-event": "^7.2.1",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-scripts": "3.4.1",
"semantic-ui-css": "^2.4.1",
"semantic-ui-react": "^1.1.1",
"solc": "^0.4.25",
"web3": "^1.2.11"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": "react-app"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}
- 创建目录与文件
[step-1] goland终端输入图中命令,特别注意当前所在目录
[step-2]根目录创建合约目录contracts,创建合约lottery.sol文件,命令如图
最终效果如图
[3].彩票合约 lottery.sol
[step-1] Google浏览器,打开Remix在线编译器(选择0.4.24) http://remix.ethereum.org/
[step-2] 在线测试代码Lottery.sol
pragma solidity ^0.4.24;
contract Lottery{
// ---整体逻辑
//1. 管理员:负责开奖和退奖,添加modifier函数限定两个函数
//2. 彩民池:address[] players,开奖退奖都要清空
//3. 当前期数:每次开奖退奖后都要加1
//4. 开奖:
// (1)求随机数,确定中奖地址 --这里需要注意
// (2)分配奖金池,分为奖金和管理费
// (3)给中奖地址和管理员地址转钱
// (4)期号+1,清空彩民池
//5. 退奖:
// (1)根据数组长度,for循环地址转钱
// (2)期号+1,清空彩民池
//6. 投注play:
// (1)限制条件:require(要求投注金额必须是1个以太币)
// (2)将投注地址添加到彩民池子
//变量,这里public,下面也用getXXX形式返回了。
//管理员地址
address public manager;
//中奖彩民
address public winner;
//第round期
uint256 public round = 1;
//所有参与的彩民(管理员也可以参与游戏)
address[] public players;
//构造函数,部署合约的人就是管理员
constructor() public{
manager = msg.sender;
}
//---彩民投注函数
//1. 合约⾥⾯的单位默认为 wei ,需要进⾏ ether 修饰,也可以使⽤ 1 * 10 ** 18 进⾏转换。
//2. payable关键字必须添加,否则⽆法购买
//3. msg.value全局变量能够接收到交易中的 value 字段
//4. 数组添加使⽤ push
function play() payable public{
//require限定投注金额为1ETH
require(msg.value == 1 ether);
//将投注者添加到彩民池
players.push(msg.sender);
}
//---管理员开奖函数,用onlyManager限定权限
//1. 随机中奖需要⼀个随机的索引值,我们使⽤难度值,当前时间,参与⼈数作为种⼦⽣成⼀个⼤的 数字⽣成索引。
//2. 开奖前要注意校验有效性,如果⽆⼈参与可以不开奖。
//3. 本轮结束后 round++ ,进⼊下⼀轮。
//4. 删除所欲的参与⼈,注意delete只是删除内部元素,不会删除 players 。
function KaiJiang() onlyManager public{
//随机一个下标值,表示获奖者
bytes memory tmp1 = abi.encodePacked(block.timestamp, block.difficulty, players.length);
bytes32 tmp2 = keccak256(tmp1);
uint256 tmp3 = uint256(tmp2);
//确定获奖者地址
uint256 index = tmp3 % players.length;
winner = players[index];
//根据9-1分成规则转钱
uint256 contractMoney = address(this).balance;
uint256 winnerMoney = contractMoney / 100 * 90;
uint256 managerMoney = contractMoney - winnerMoney;
winner.transfer(winnerMoney);
manager.transfer(managerMoney);
//本期结束后期数+1,并清空彩民池
round++;
delete players;
}
//---管理员退奖
function TuiJiang() onlyManager public{
//遍历数组,逐一转账
for(uint i = 0; i < players.length; i++){
players[i].transfer(1 ether);
}
//期数+1
round++;
//清空彩民池
delete players;
}
//---修饰器
modifier onlyManager{
//限定作用,非管理员不允许调用开奖和退奖函数
require(msg.sender == manager);
_;
}
//返回奖池金额
function getBalance() view public returns(uint){
return address(this).balance;
}
//返回奖池人数
function getPlayersLength() view public returns(uint){
return players.length;
}
//以数组形式返回奖池人地址
function getPlayers() view public returns(address []){
return players;
}
//返回管理员
function getManager() view public returns(address){
return manager;
}
//返回管理员
function getWinner() view public returns(address){
return winner;
}
//返回当前期号
function getRound() view public returns(uint256){
return round;
}
}
[4].编译合约 01-compile.js
//1.导入相关包
let solc = require('solc')
let fs = require('fs')
//2.读取合约
let sourceCode = fs.readFileSync('./contracts/Lottery.sol', 'utf-8')
//3.编译合约
//将1设置为第二个参数将激活优化器
let output = solc.compile(sourceCode, 1)
//4.导出合约
//console.log(output)
module.exports = output[ 'contracts'][':Lottery']
[5].部署合约 02-deploy.js
//1. 引入
let {bytecode, interface} = require('./01-compile')
let Web3 = require('web3')
//let HDWalletProvider = require('truffle-hdwallet-provider')
//2. new一个web3实例
let web3 = new Web3();
//3. 设置网络,这里用Ganache本地测试网络环境,自行搜索安装
//助记词
//let terms = '注意这里需要输入助记词';
let netIp = 'http://localhost:7545'
//let provider = new HDWalletProvider(terms, netIp)
//const web3 = new Web3();
web3.setProvider(netIp)
let contract = new web3.eth.Contract(JSON.parse(interface))
let deploy = async () => {
//4. 先获取所有的账户
let accounts = await web3.eth.getAccounts() //获取账户列表
console.log('account:', accounts)
//5. 执行部署
let instance = await contract.deploy({
data:bytecode, //合约的bytecode
//argument : ['HelloWorld']
}).send({
from : accounts[0], //部署合约人(管理员地址),需要从这里扣钱!!!
gas : '6721975',
gasPrice : '1'
})
console.log('instance address : ', instance.options.address)
}
deploy()
[6]. 从区块链获取合约实例
initWeb3.js
注意重要更新,很重要
let Web3 = require('web3')
let web3 = new Web3();
//重要更新:为了保护隐私,默认关闭,需要手动开启,才能获取用户当前地址
//information : https://medium.com/metamask/https-medium-com-metamask-breaking-change-injecting-web3-7722797916a8
window.ethereum.enable();
//说明:
//实例化web3需要一个provider(服务商),管理员部署的时候用自己的账号
//但是彩民参与这个活动,投注要用自己账号的钱
//实现
//1.用Ganache的某个账户地址的私钥,在MetaMask中创建一个账户
//2.在浏览器启动后,MetaMask会向浏览器注入一个web3实例
//3.利用window.web3.currentProvider获得实例
web3.setProvider(window.web3.currentProvider)
console.log("Injected web3 found!")
module.exports = web3
lottery.js
注意:不要直接复制abi和address,将合约放到在线编译器http://remix.ethereum.org/中编译通过,从在线编译器中复制abi
let web3 = require('../utils/initWeb3')
//这里可以从remix solidity在线编译器中直接获取
const abi = [{
"constant": false,
"inputs": [],
"name": "KaiJiang",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
}, {
"constant": false,
"inputs": [],
"name": "play",
"outputs": [],
"payable": true,
"stateMutability": "payable",
"type": "function"
}, {
"constant": false,
"inputs": [],
"name": "TuiJiang",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
}, {"inputs": [], "payable": false, "stateMutability": "nonpayable", "type": "constructor"}, {
"constant": true,
"inputs": [],
"name": "getBalance",
"outputs": [{"name": "", "type": "uint256"}],
"payable": false,
"stateMutability": "view",
"type": "function"
}, {
"constant": true,
"inputs": [],
"name": "getManager",
"outputs": [{"name": "", "type": "address"}],
"payable": false,
"stateMutability": "view",
"type": "function"
}, {
"constant": true,
"inputs": [],
"name": "getPlayers",
"outputs": [{"name": "", "type": "address[]"}],
"payable": false,
"stateMutability": "view",
"type": "function"
}, {
"constant": true,
"inputs": [],
"name": "getPlayersLength",
"outputs": [{"name": "", "type": "uint256"}],
"payable": false,
"stateMutability": "view",
"type": "function"
}, {
"constant": true,
"inputs": [],
"name": "getRound",
"outputs": [{"name": "", "type": "uint256"}],
"payable": false,
"stateMutability": "view",
"type": "function"
}, {
"constant": true,
"inputs": [],
"name": "getWinner",
"outputs": [{"name": "", "type": "address"}],
"payable": false,
"stateMutability": "view",
"type": "function"
}, {
"constant": true,
"inputs": [],
"name": "manager",
"outputs": [{"name": "", "type": "address"}],
"payable": false,
"stateMutability": "view",
"type": "function"
}, {
"constant": true,
"inputs": [{"name": "", "type": "uint256"}],
"name": "players",
"outputs": [{"name": "", "type": "address"}],
"payable": false,
"stateMutability": "view",
"type": "function"
}, {
"constant": true,
"inputs": [],
"name": "round",
"outputs": [{"name": "", "type": "uint256"}],
"payable": false,
"stateMutability": "view",
"type": "function"
}, {
"constant": true,
"inputs": [],
"name": "winner",
"outputs": [{"name": "", "type": "address"}],
"payable": false,
"stateMutability": "view",
"type": "function"
}]
//注意这里是合约地址,从deploy.js返回值中获取
const address = '0x6988851A341E179C29733e8C824D5658A5572507'
let lottery = new web3.eth.Contract(abi, address)
module.exports = lottery
[7].完善界面
ui.js
import React from 'react'
import { Card, Icon, Image, Statistic, Button, Label} from 'semantic-ui-react'
const CardExampleCard = (props) => (
<Card>
<Image src='images/logo.jpg' wrapped ui={false} />
<Card.Content>
<Card.Header>中国体育彩票</Card.Header>
<Card.Meta>
<p>管理员地址:</p>
<Label size='mini'>
<Icon name='hand point right' />{props.manager}
</Label>
<p>当前地址:</p>
<Label size='mini'>
<Icon name='hand point right' />{props.account}
</Label>
</Card.Meta>
<Card.Description>
每晚八点半准时开奖,不见不散!!!
</Card.Description>
</Card.Content>
<Card.Content extra>
<a>
<Icon name='user' />
{props.playerCounts}人参与
</a>
</Card.Content>
<Card.Content extra>
<Statistic color='red'>
<Statistic.Value>{props.balance}ETH</Statistic.Value>
<Statistic.Label>奖金池</Statistic.Label>
</Statistic>
</Card.Content>
<Card.Content extra>
<Statistic color='blue'>
<Statistic.Value>第{props.round}期</Statistic.Value>
<a href='www.baidu.com'>点我查看历史交易记录</a>
</Statistic>
</Card.Content>
<Button animated='fade' color='orange' onClick={props.play} loading={props.isPlaying}>
<Button.Content visible>投注产生希望</Button.Content>
<Button.Content visible>购买放飞梦想</Button.Content>
</Button>
<Button inverted color='red' style={{display:props.isShowButton}} onClick={props.Kaijiang} loading={props.isDrawing}>
开奖
</Button>
<Button inverted color='green' style={{display:props.isShowButton}} onClick={props.Tuijiang} loading={props.isDrawBacking}>
退奖
</Button>
</Card>
)
export default CardExampleCard
App.js
import React,{Component} from 'react';
import CardExampleCard from './display/ui'
let web3 = require('./utils/initWeb3')
let lottery = require('./eth/lottery')
class App extends Component{
constructor(props) {
super(props);
this.state = {
manager : '',
round:'',
winner:'',
playerCounts:0,
balance:0,
players:[],
account : '',
};
}
async componentWillMount(){
let accounts = await web3.eth.getAccounts()
let manager = await lottery.methods.manager().call();
let round = await lottery.methods.getRound().call();
let winner = await lottery.methods.getWinner().call();
let playerCounts = await lottery.methods.getPlayersLength().call();
let balanceWei = await lottery.methods.getBalance().call();
let balance = web3.utils.fromWei(balanceWei, 'ether')
let players = await lottery.methods.getPlayers().call();
this.setState({
manager,
round,
winner,
playerCounts,
balance,
players,
account:accounts[0],
isPlaying:false,
isDrawing:false,
isDrawBacking:false,
isShowButton:accounts[0] === manager ? 'inline' : 'none',
});
}
play = async() =>{
console.log("Play Button Clicked!!!");
this.setState({isPlaying:true})
try{
let accouts = await web3.eth.getAccounts();
await lottery.methods.play().send({
from:accouts[0],
value:1*10**18
})
alert(`Success: play successfully!`);
this.setState({isPlaying:false});
window.location.reload(true);
}catch (e) {
alert(`Error: play failed! ${e}`);
this.setState({isPlaying:false});
}
}
Kaijiang = async() =>{
console.log("Kaijiang Button Clicked!!!");
this.setState({isDrawing:true})
try{
let accouts = await web3.eth.getAccounts();
await lottery.methods.KaiJiang().send({
from:accouts[0],
})
let winner_ = await lottery.methods.getWinner().call();
alert(`开奖喽!中奖者为${winner_}`);
this.setState({isDrawing:false});
window.location.reload(true);
}catch (e) {
alert(`Error: kaijiang failed! ${e}`);
this.setState({isDrawing:false});
}
}
Tuijiang = async() =>{
console.log("Tuijiang Button Clicked!!!");
this.setState({isDrawBacking:true})
try{
let accouts = await web3.eth.getAccounts();
await lottery.methods.TuiJiang().send({
from:accouts[0],
})
alert(`退奖成功!!!`);
this.setState({isDrawBacking:false});
window.location.reload(true);
}catch (e) {
alert(`Error: Tuijiang failed! ${e}`);
this.setState({isDrawBacking:false});
}
}
render() {
return (
<div>
<CardExampleCard
manager={this.state.manager}
round={this.state.round}
winner={this.state.winner}
balance={this.state.balance}
players={this.state.players}
playerCounts={this.state.playerCounts}
account={this.state.account}
play={this.play}
Tuijiang={this.Tuijiang}
Kaijiang={this.Kaijiang}
isPlaying={this.state.isPlaying}
isDrawing={this.state.isDrawing}
isDrawBacking={this.state.isDrawBacking}
isShowButton={this.state.isShowButton}
/>
</div>
);
}
}
export default App;
[8].最终效果
创作声明
本文是作者学习"传智播客"旗下"区块链"课程时,基于自己思考,实现并总结的学习笔记。
本文项目思路,图片源于"传智播客"的学习资料,若有不当,请联系删除。
若有需要,请支持并购买"传智播客"旗下正版学习资料。
备注
一枚区块链萌新,希望同大家多多学习交流!!!
来源:oschina
链接:https://my.oschina.net/u/4270977/blog/4478352