以太坊畢業設計DAPP開發-彩票的設計與實現

以太坊DAPP開發-彩票的設計與實現
一.項目背景
​ 傳統的彩票網站存在暗箱操作,容易貪污跑路的問題,而基於以太坊的彩票網站,則有着公開,公正,公平的優點。

二.業務需求
1.全民參與(任何地址都可以投注)

2.每個人每次只能投1個ether(相當於2元1注)

3.每個人可以買多注

4.設置一個管理員,負責:

定期開獎
臨時退獎(防止有特殊情況)
三.項目框架圖

項目模塊交互演示圖

四.合約設計
合約需要的狀態變量
​ 1.管理員:manager ,address類型

​ 2.記錄所有的彩民的地址集合:players,address[]類型

​ 3.第幾期:round int

​ 4.上一期的中獎地址:winner,address

合約中的方法
​ 1.參與投獎:play() payable(任何人都可以調用,調用時轉入1ether到合約)

​ 2.管理員專用

​ -開獎:draw()選擇一個隨機的地址,將合約的錢轉入這個地址;

​ -退獎:undraw()遍歷所有的彩民,依次向彩民池中的地址轉賬,每人轉賬1ether。

注意點:

退獎和開獎時需要花費的手續費是管理員地址賬戶中的錢;
投獎時花費的手續費是用戶賬戶中的錢;
一句話:誰調用合約中的方法就花費誰的錢;
對於用戶來說,進出都是1eth永遠不變;
合約代碼
pragma solidity ^0.4.25;

contract Lottery{
//管理員地址
address public manager;
//所有彩民
address[] public players;
//彩票期數
uint public round;
//上一期中獎地址
address public winner;
//添加管理員地址
constructor() public{
manager=msg.sender;
}

//參與投獎
function play() public payable{
    //要求調用時轉入
    require(msg.value==1 * 10 ** 18);
    players.push(msg.sender);
}
//管理員開獎
function draw() public onlyManager{
    //以太坊中沒有提供隨機數生成方法,可以隨機生成一個哈希數,並將它對數組長度取餘
    //取當前的難度值,時間戳和彩民人數作爲隨機數的種子
    bytes memory info = abi.encodePacked(block.difficulty,block.timestamp,players.length);
    bytes32 hash = keccak256(info);
    uint index = uint(hash)%players.length;
    winner = players[index];
    winner.transfer(address(this).balance);
    //彩民池清空同時期數加1
    delete players;
    round++;
}

//管理員退獎
function undraw() public onlyManager{
    //遍歷彩民數組,依次向彩民轉賬
    for(uint256 i=0;i<players.length;i++){
        players[i].transfer(1 ether);
    }
    delete players;
    round++;
}
/*******************輔助函數*************************/
//獲取獎金池額度
function getBalance() public view returns(uint256){
    return address(this).balance;
}
//獲取當前彩民
function getPlayers() public view returns(address[]){
    return players;
}
//添加修飾器
modifier onlyManager(){
    require(msg.sender==manager);
    _;
}

}
五.前端部署(react)
創建react空工程
npm i -g create-react-app //安裝react腳手架
create-react-app lottery //創建項目文件夾
npm run start //運行react空工程
清理react空工程

​ src文件夾下只保留App.js和index.js;

​ App.js以及Index.js文件夾下保留的內容:

//App.js
import React from ‘react’;

function App() {
return (


hellowrold!

);
}
export default App;

//index.js
import React from ‘react’;
import ReactDOM from ‘react-dom’;

import App from ‘./App’;

ReactDOM.render(, document.getElementById(‘root’));
​ 瀏覽器中輸入localhost:3000看到如下結果表示運行成功:

六.合約部署測試
安裝solc編譯器
//執行該命令必須與package.json位於同一文件夾下
cd lottery
npm install [email protected] --save
安裝web3
//執行該命令必須與package.json位於同一文件夾下
cd lottery
node install [email protected] --save
啓動命令行版本Ganache
//如果沒有命令行版本Ganache需要先進行安裝
npm install ganache-cli -g
//啓動巧克力
ganache-cli
​ 啓動之後如果出現如下情況表示Gananche安裝成功:

使用指定方式啓動Ganache
​ 每次輸入ganache-cli啓動後,會有如下特點:

​ 默認的服務爲:Listening on 127.0.0.1:8545。
​ 每次啓動時,10個地址是變化的,不太方便。
​ 在這裏我們使用參數指定啓動的數據:

​ genache-cli -m 指定“助記詞”,

​ -h 指定“ip”,

​ -p “port”。

獲取助記詞的方法
ganache-cli
​ 啓動後可以看到如下界面:

如圖中所示,Mnemonic即爲助記詞,拷貝這些助記詞;

通過助記詞啓動ganache-cli
ganache-cli -h 127.0.0.1 -p 7545 -m “caught flower unique despair swift convince tip royal alter tuition token marble”
導入助記詞到MetaMask
​ 啓動MetaMask,選擇從助記詞還原

​ 將助記詞拷貝至wallet seed並設置新密碼

此時MetaMask已經連接到ganache-cli,可以通過MetaMask對ganache-cli中的賬戶進行轉賬或查看操作,從而模擬用戶的真實操作。

合約編譯部署
在lottery文件夾下創建compile.js,並編寫代碼如下:

let fs = require(‘fs’)
let solc = require(‘solc’)

//指定utf-8,返回的就是一個string字符串,下面編譯的時候,就不需要使用toString()方法
//如果不指定utf-8,返回的就是Buffer, 那麼下面編譯時,需要使用toString()方法
let contractInfo = fs.readFileSync(’./lottery.sol’, ‘utf-8’)

let compiledInfo = solc.compile(contractInfo, 1)
console.log(compiledInfo)

//JSON.parse(字符串) => 轉換成一個json對象
// JSON.stringfy(json對象) => 轉換成一個string

let res = fs.writeFileSync(’./compileInfo.json’, JSON.stringify(compiledInfo), ‘utf-8’)

module.exports = compiledInfo[‘contracts’][’:Lottery’]
在lottery文件夾下創建deploy.js,並編寫代碼如下:

let {
bytecode,
interface
} = require(’./compile’)

let Web3 = require(‘web3’)

console.log(‘bytecode :’, bytecode)
console.log(‘interface :’, interface)

//初始化web3
//ganache-cli本地測試環境,不需要指定助記詞就可以使用裏面的賬戶
let web3 = new Web3(‘http://127.0.0.1:7545’)

let deploy = async () => {
let accounts = await web3.eth.getAccounts()

console.log('accounts : ', accounts)

//填寫abi, 第二參數是合約地址,部署的時候不用填寫
let contract = new web3.eth.Contract(JSON.parse(interface))

//填寫bytecode和構造函數參數
contract.deploy({
    data: bytecode,
    // arguments:[], //如果沒有構造函數參數,可以不寫這個字段
}).send({
    from: accounts[0],
    // to :  //部署合約時,不需要填寫to字段
    value: 0,
    // gas : '8000000000', 如果太小,創建交易時gas不足,會失敗。如果過多,會提示超出gas上限,最多800萬gas
    gas: '800000'
}).then(res => {
    console.log('新部署合約的地址爲: ', res.options.address)
}).catch(err => {
    console.log('部署合約失敗:', err)
})

}

deploy()
執行如下命令:

node compile.js
node deploy.js

得到上述地址表示合約部署成功。

七.項目模塊設計與實現
​ 根據項目框架圖可知,項目整體採用MVC架構,其中controller層使用react工程初始化的App.js,Model層和View層需要另外創建。

​ 在lottery/src文件夾下創建eth文件夾和display文件夾;

cd ./lottery/src
mkdir eth //model層代碼
mkdir display//view層代碼
獲取合約實例
cd ./lottery/src/eth
touch getInstance.js
​ 獲取合約實例需要通過ABI和部署地址,ABI可以通過我們執行compile.js中的compile.json獲取,也可以通過remix在線編譯器獲取;

地址可以通過編譯deploy.js後的地址獲取。

getInstance.js代碼如下:

//根據ABI和部署地址獲得合約實例
const ABI = [ { “constant”: false, “inputs”: [], “name”: “draw”, “outputs”: [], “payable”: false, “stateMutability”: “nonpayable”, “type”: “function” }, { “constant”: true, “inputs”: [], “name”: “getBalance”, “outputs”: [ { “name”: “”, “type”: “uint256” } ], “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”: “manager”, “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”: false, “inputs”: [], “name”: “play”, “outputs”: [], “payable”: true, “stateMutability”: “payable”, “type”: “function” }, { “constant”: true, “inputs”: [], “name”: “winner”, “outputs”: [ { “name”: “”, “type”: “address” } ], “payable”: false, “stateMutability”: “view”, “type”: “function” }, { “constant”: false, “inputs”: [], “name”: “undraw”, “outputs”: [], “payable”: false, “stateMutability”: “nonpayable”, “type”: “function” }, { “constant”: true, “inputs”: [ { “name”: “”, “type”: “uint256” } ], “name”: “players”, “outputs”: [ { “name”: “”, “type”: “address” } ], “payable”: false, “stateMutability”: “view”, “type”: “function” }, { “inputs”: [], “payable”: false, “stateMutability”: “nonpayable”, “type”: “constructor” } ]
const Address = ‘0x2F1171a6F34ad4407e362C0F801D7883104D7FDa’;

//實例化web3
let Web3 = require(‘web3’);
let web3 = new Web3(‘http://127.0.0.1:7545’);

//獲取合約實例
let lotteryIntance = new web3.eth.Contract(ABI,Address);
//將合約實例導出,這樣在其他地方就可以直接與合約交互
//如果只導出一個對象,可以使用default修飾,這樣在導入時可以定義一個其他的名字
export default lotteryIntance
改寫App.js代碼如下:

import React,{Component} from ‘react’;
import instance from ‘./eth/getInstance’

class App extends Component{
//生命週期函數,在頁面掛載的時候自動執行
async componentDidMount(){
let manager = await instance.methods.manager().call()
console.log(‘管理員地址:’,manager);
}
render(){
return (


hellowrold!

);
}
}
export default App;
​ 打開瀏覽器,訪問localhost:3000,按下F12,如果看到控制檯輸出管理員地址則表示調用成功。

此時表明我們已經可以通過頁面獲取到合約實例;

state狀態變量
​ state:內置狀態變量,在類內進行數據傳遞;

State={ a:’’, }

讀操作:let a = this.state.a;

寫操作:this.setState({a:‘hello’});
react中標籤內的變量需要使用{}包裹起來,否則會當做字符串顯示

繼續改寫App.js如下:

import React,{Component} from ‘react’;
import instance from ‘./eth/getInstance’

class App extends Component{
//狀態變量用於傳遞數據
state={
manager:’’,
round:0,
winner:’’,
allPlayers:[],
balance:0,
}
//生命週期函數,在頁面掛在的時候自動執行
async componentDidMount(){
//管理員地址:manager
let manager = await instance.methods.manager().call();
//當前的期數:round
let round = await instance.methods.round().call();
//上一期中獎者地址:winner
let winner = await instance.methods.winner().call();
//玩家數組:players
let allPlayers = await instance.methods.getPlayers().call();
//合約裏邊的金額
let balance = await instance.methods.getBalance().call();
//打印上述數據
let detailInfo ={manager,round,winner,balance};
console.table(detailInfo);
//設置狀態變量
this.setState({manager,round,winner,allPlayers,balance});

}
render(){
    let {manager,round,winner,allPlayers,balance} = this.state
    return (

        <div className="App">
            <p>hellowrold!</p>
            <p>manager:{manager}</p>
            <p>round:{round}</p>
            <p>winner:{winner}</p>
            <p>allPlayers:{allPlayers}</p>
            <p>balance:{balance}</p>
        </div>
    );
}

}

export default App;
此時運行結果如下圖所示:

頁面UI設計與實現
​ 這裏簡單起見,我們不再手寫代碼實現UI而是,直接從semantic-ui上拉取一個頁面下來。

//安裝semanct-ui-react
npm install semantic-ui-react --save
//安裝semanct-ui-css
npm install semantic-ui-css --save
​ 瀏覽器內輸入網址:https://react.semantic-ui.com/views/card/#types-card

​ 點擊try-it如下圖所示

之後下方會出現一段代碼,拷貝這段代碼,粘貼到display.js下;

vim ./lottery/src/display/display.js
在App.js中引入display.js如下圖所示:

在index.js中引入semantic-ui-css,如下圖所示:

在./lottery/public文件夾下放置彩票logo圖片,並修改display.js中的圖片放置地址。注意:圖片資源地址的根目錄爲/public

最後效果如下圖所示:

修改頁面代碼
修改display.js代碼如下:

import React from ‘react’
import { Card, Icon, Image, Statistic } from ‘semantic-ui-react’

const CardExampleCard = () => (


<Card.Content>
<Card.Header>皇家彩票</Card.Header>
<Card.Meta>開獎員地址:</Card.Meta>

0x1234567890


<Card.Meta>當前地址:</Card.Meta>

0x1234567890


<Card.Description>全天24小時在線,每週一、週二、週六晚八點開獎</Card.Description>
</Card.Content>
<Card.Content extra>


10人

</Card.Content>
<Card.Content extra>

<Statistic.Value>10 ETH</Statistic.Value>
<Statistic.Label>獎金池</Statistic.Label>

</Card.Content>
<Card.Content extra>

<Statistic.Value>第0 期</Statistic.Value>
<Statistic.Label>期數</Statistic.Label>
點擊我查看交易歷史

</Card.Content>

)

export default CardExampleCard
頁面數據導入
​ props:負責多個組件之間的數據傳遞(包括:數據,函數);

​ 獲取數據涉及到不同類之間的數據傳遞,此時需要使用props;

​ getInstance.js代碼改寫如下:

​ App.js代碼改寫如下:

import React,{Component} from ‘react’;
import {instance,web3} from ‘./eth/getInstance’
import CardExampleCard from ‘./display/display’
class App extends Component{
//狀態變量用於傳遞數據
state={
manager:’’,
round:0,
winner:’’,
allPlayers:[],
balance:0,
currentAccount:’’,
}
//生命週期函數,在頁面掛在的時候自動執行
async componentDidMount(){
//獲取所有賬戶地址
let accounts = await web3.eth.getAccounts();
//獲取當前賬戶地址
let currentAccount = accounts[1];
//管理員地址:manager
let manager = await instance.methods.manager().call();
//當前的期數:round
let round = await instance.methods.round().call();
//上一期中獎者地址:winner
let winner = await instance.methods.winner().call();
//玩家數組:players
let allPlayers = await instance.methods.getPlayers().call();
//合約裏邊的金額
let balance = await instance.methods.getBalance().call();
//打印上述數據
let detailInfo ={manager,round,winner,balance};
console.table(detailInfo);
//設置狀態變量
this.setState({manager,round,winner,allPlayers,balance,currentAccount});

}
render(){
    let {manager,round,winner,allPlayers,balance,currentAccount} = this.state
    return (

        <div className="App">
            <CardExampleCard allData={this.state}/>
        </div>
    );
}

}

export default App;
​ display.js代碼改寫如下:

import React from ‘react’
import { Card, Icon, Image, Statistic } from ‘semantic-ui-react’

const CardExampleCard = (props) => {
let allData = props.allData;
let{manager,round,winner,allPlayers,balance,currentAccount}=allData;
return(


<Card.Content>
<Card.Header>皇家彩票</Card.Header>
<Card.Meta>開獎員地址:</Card.Meta>

{manager}


<Card.Meta>當前地址:</Card.Meta>

{currentAccount}


<Card.Meta>上一期中獎地址:</Card.Meta>

{winner}


<Card.Description>全天24小時在線,每週一、週二、週六晚八點開獎</Card.Description>
</Card.Content>
<Card.Content extra>


{allPlayers.length}人

</Card.Content>
<Card.Content extra>

<Statistic.Value>{balance} ETH</Statistic.Value>
<Statistic.Label>獎金池</Statistic.Label>

</Card.Content>
<Card.Content extra>

<Statistic.Value>第{round} 期</Statistic.Value>
<Statistic.Label>期數</Statistic.Label>
點擊我查看交易歷史

</Card.Content>

);}

export default CardExampleCard
添加按鈕
​ 頁面需要三個按鈕,分別是開獎、退獎、投注;

App.js頁面修改如下:

按鈕業務邏輯的實現
​ 在App.js中添加如下代碼:

//調用的函數
play=async()=> {
try{
await instance.methods.play().send({
from:this.state.currentAccount,
value:(1*10)**18
});
alert(‘投注成功’);
window.location.reload(true);
}catch{
alert(‘投注失敗’);
window.location.reload(true);
}

}
draw=async()=>{
    try{
        await instance.methods.undraw().send({
            from:this.state.currentAccount,
        });
        //在這裏重新設置中將着
        let winner = await instance.methods.winner().call();
        console.log('中獎地址:',winner);
        alert('開獎成功');
        window.location.reload(true);
    }catch{
        alert('開獎失敗');
        window.location.reload(true);
    }
}
undraw=async()=>{
    try{
        await instance.methods.undraw().send({
            from:this.state.currentAccount,
        });
        alert('退獎成功');
        window.location.reload(true);
    }catch{
        alert('退獎失敗');
        window.location.reload(true);
    }
}
render(){
    let {manager,round,winner,allPlayers,balance,currentAccount} = this.state
    return (

        <div className="App">
            <CardExampleCard
                allData={this.state}
                play={this.play}
                draw={this.draw}
                undraw={this.undraw}
            />
        </div>
    );
}

鏈接MetaMask
​ 當前項目中,我們的用戶是固定的,每次取Ganace中account[1]的地址,在實際業務中,這裏需要使用用戶自己的provider。

​ 在安裝MetaMask後,MetaMask會在瀏覽器中註冊一個自己的web3實例,MetaMask在鏈接網絡之後,會生成一個自己的web3 provider。

​ 修改getInstance.js如下:

八.項目優化
單位優化
​ 在運行過程中,我們發現投注的單位是wei,但是頁面顯示要求的單位是ether,會出現如下問題:

修改App.js代碼如下:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章