根据去中心化的课程评分系统白皮书,我们草拟了评分系统合约,实现了打分可追溯和打分次数记录等基本功能,并将其部署在了私有链上。此外,初步实现了DAPP功能,并用 node.js 搭建了页面。未来的计划包括:打分合约的撰写和部署,以及打分界面的设计和优化。
实验依赖
Node.js
节点包管理器(NPM)在本次实验中作为web开发的工具。家喻户晓的npm这里就不再赘述了,用homebrew安装node.js:
brew install node
显示版本成功为安装好了:
npm -v
Truffle
Truffle🍰(松露)——“聪明的合同更甜蜜”,是一个简洁的智能合约开发框架。通过下载demo,我们可以很快的上手部署合约,并且Truffle最让人惊叹之处在于它甚至提供了和node.js 协同开发web Ui的interface,真的因此十分符合初级Dapper的需求。
用node.js安装Truffle:
npm install -g truffle
Ganache
Truffle Suite提供了一个很好用的私有链工具——Ganache。Ganache可以快速启动个人Ethereum区块链,可以使用它来运行测试、执行命令和检查状态,同时控制链的操作方式。这里用Ganache就是为了方便创建accounts。
Ganache可以从这里下载。
Metamask
”小狐狸🦊“——Metamask 是 Google Chrome 浏览器的扩展,将以太坊与 Google Chrome 结合,在 Chrome 浏览器上运行以太坊 DApps,以及身份识别的工具。于是,它就具备了类似 Mist 的钱包功能,允许用户管理自己的账户,通过 Web3 JavaScript API,让 DApp 与以太坊区块链实现交互。从Chrome Extension Store里就能下载Metamask,注册一个账户就能用了(虽然没钱💰)。当然,我们之后用到的账户并不是这个注册的账户,而是Ganache上的账户。在Chrome上注册账户并登陆。
智能合约
Truffle框架
创建项目——Score Chain:
mkdir scorechain
cd scorechain
使用[truffleframework.com/boxes/][truffleframework.com/boxes/]快速启动和运行。安装宠物商店demo:
truffle unbox pet-shop
当我们的框架安装好了的时候,目录结构如图:
truffle.js 是truffle框架和ganache网络连接的配置文件,host一般用localhost,端口取决于ganache的RPC 服务器(如果端口错了后期设计网页会一直 loading)。文件内容为:
module.exports = {
networks: {
development: {
host: "127.0.0.1",
port: 7545,
network_id: "*"
}
}
};
src是网页开发源代码目录,暂时只用到了app.js 和 index.html 这个文件。app.js这个文件是前端和后端的interface,是整个Dapp十分重要的一部分。**index.html **是前端设计文件。
node_modules 是node.js的模块,暂时不需要。
contracts 就是合约目录啦!之后我们的合约就是在这里完成的。合约写好了之后需要部署在我们用Ganache创建的私有链上,需要在migrations文件夹下写部署文件。当部署在私有链上的时候,truffle框架会为我们生成build,编译我们的合约。
合约拟写
在contracts目录下,建立合约文件,用>=0.4.20 <0.6.1 的 solidity编写合约Score(打分)。
构建学生结构体:
struct Student {
uint id;
string name;
uint selectCount;
}
建立关于学生和TA的映射,并创建变量被评次数:
// Store TAs
mapping(address => bool) public TAs;
// Store Students
mapping(uint => Student) public students;
// Store Students Count
uint public scoredTimes;
定义添加学生和TA打分函数:
function addStudent (string _name) private {
scoredTimes ++;
students[scoredTimes] = Student(scoredTimes, _name, 0);
}
function select (uint _studentId) public {
// require that they haven't selected before
require(!TAs[msg.sender]);
// require a valid student
require(_studentId > 0 && _studentId <= scoredTimes);
// record that TA has selected
TAs[msg.sender] = true;
// update student select Count
students[_studentId].selectCount ++;
// trigger selected event
selectEvent(_studentId);
}
最后还需要定义一个选择学生事件:
// select event
event selectEvent (
uint indexed _studentId
);
至此,合约拟写成功!
合约部署
为了将合约部署到Ganache私有链上,还需要一个部署文件,在migrations目录下部署合约:
var Score = artifacts.require("./Score.sol");
module.exports = function(deployer) {
deployer.deploy(Score);
};
打开Ganache,看看RPC 服务器(7545)是否和truffle.js 对应:
部署合约到Ganache私有链:
truffle migrate --reset // 非首次部署要加reset
出现下图为成功:
Using network 'development'.
Running migration: 1_initial_migration.js
Replacing Migrations...
... 0x8006a2052e71652571a823fc4a33f5f88ea1bc76972ef08dafbaade016e330ab
Migrations: 0x64745cba2a428767a9c6518da9bc5752492fec22
Saving successful migration to network...
... 0x42cc99e3517718bb12f89b90f8d86e3e4aad0b91a4a0b28331cf89c817de89c2
Saving artifacts...
Running migration: 2_deploy_contracts.js
Replacing Score...
... 0x24a274ae9c7fba6142290ebcf5b966faafbc4852f0b752022627679fb6bc8c08
Score: 0xebde42adb74d844988238720c4e50feada2f6f2f
Saving successful migration to network...
... 0x7d1aedce77df01a0e77f59ca9f55973fb90ba9cf7d61469f8fcc8dbfb7d1067b
Saving artifacts...
打开truffle console,声明合约实例,检查我们部署的合约:
$ truffle console // 进入console
声明一个实例:
Score.deployed().then(function(instance) { app = instance })
看看我们有多少个学生(6个):
app.studentsNum()
// 显示 BigNumber { s: 1, e: 0, c: [ 6 ] }
这就说明部署成功了,我们再看看默认的部署用户,Ganache的用户0:嗯,果然它从原来的100eth变少了,说明部署合约确实有以太币的花费!
这时候truffle框架为我们自动生成build/contracts,目录下的Score.json是合约的可执行文件。之后会在我们的客户端开发中用到。
测试文件
为了验证我们部署的合约是否正确,还需要设计几个测试:
touch ./test/score.js
score.js是我们的测试文件。打开文件,设计测试函数如下:
it("initializes with six students", function(){...};
it("it initializes the students with the correct values", function() {};
it("allows a TA to cast a select", function() {};
it("throws an exception for invalid students", function() {};
it("throws an exception for double selecting", function() {};
查看测试结果:
truffle test
五个测试都通过了!!!
合约客户端
接口设计
初始化Web3:
initWeb3: function() {
if (typeof web3 !== 'undefined') {
App.web3Provider = web3.currentProvider;
web3 = new Web3(web3.currentProvider);
} else {
App.web3Provider = new Web3.providers.HttpProvider('http://localhost:7545');
web3 = new Web3(App.web3Provider);
}
return App.initContract();
},
将合约初始化:
initContract: function() {
$.getJSON("Score.json", function(score) {
App.contracts.Score = TruffleContract(score);
App.contracts.Score.setProvider(App.web3Provider);
App.listenForEvents();
return App.render();
});
},
等待合约emit给观察者(也就是我们):
// Listen for events emitted from the contract
listenForEvents: function() {
App.contracts.Score.deployed().then(function(instance) {
instance.selectedEvent({}, {
fromBlock: 0,
toBlock: 'latest'
}).watch(function(error, event) {
console.log("event triggered", event)
App.render();
});
});
},
render就是我们主要接口了:首先加载了6个学生的信息;然后加载了合约的内容,包括可选学生和目前学生的评分情况。
render: function() {
var scoreInstance;
var loader = $("#loader");
var content = $("#content");
loader.show();
content.hide();
// Load account data
web3.eth.getCoinbase(function(err, account) {
if (err === null) {
App.account = account;
$("#accountAddress").html("Your Account: " + account);
}
});
// Load contract data
App.contracts.Score.deployed().then(function(instance) {
scoreInstance = instance;
return scoreInstance.studentsNum();
}).then(function(studentsNum) {
var studentsResults = $("#studentsResults");
studentsResults.empty();
var studentsSelect = $('#studentsSelect');
studentsSelect.empty();
for (var i = 1; i <= studentsNum; i++) {
scoreInstance.students(i).then(function(student) {
var id = student[0];
var name = student[1];
var scoredTimes = student[2];
// Render student Result
var studentTemplate = "<tr><th>" + id + "</th><td>" + name + "</td><td>" + scoredTimes + "</td></tr>"
studentsResults.append(studentTemplate);
// Render student ballot option
var studentOption = "<option value='" + id + "' >" + name + "</ option>"
studentsSelect.append(studentOption);
});
}
return scoreInstance.TAs(App.account);
}).then(function(hasSelected) {
// Do not allow a user to select
if(hasSelected) {
$('form').hide();
}
loader.hide();
content.show();
}).catch(function(error) {
console.warn(error);
});
},
最后定义了事件的发生:
castSelect: function() {
var studentId = $('#studentsSelect').val();
App.contracts.Score.deployed().then(function(instance) {
return instance.select(studentId, { from: App.account });
}).then(function(result) {
// Wait for selects to update
$("#content").hide();
$("#loader").show();
}).catch(function(err) {
console.error(err);
});
}
};
前端设计
最后我们还设计了一个和谐友好的前端:
<!DOCTYPE html>
<html lang="en">
<body background="https://ws3.sinaimg.cn/large/006tNbRwgy1fy9tabq6tsj30u00yqq3p.jpg">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- The above 3 meta tags *must* come first in the head; any other head content must come *after* these tags -->
<title>Score Chain</title>
<!-- Bootstrap -->
<link href="css/bootstrap.min.css" rel="stylesheet">
<!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
<!--[if lt IE 9]>
<script src="https://oss.maxcdn.com/html5shiv/3.7.3/html5shiv.min.js"></script>
<script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
<![endif]-->
</head>
<body>
<div class="container" style="width: 500px;">
<div class="row">
<div class="col-lg-12">
<h1 class="text-center">Score Chain</h1>
<hr/>
<br/>
<div id="loader">
<p class="text-center">Loading...</p>
</div>
<div id="content" style="display: none;">
<table class="table">
<thead>
<tr>
<th scope="col">Id</th>
<th scope="col">Name</th>
<th scope="col">Selects</th>
</tr>
</thead>
<tbody id="studentsResults">
</tbody>
</table>
<hr/>
<form onSubmit="App.castSelect(); return false;">
<div class="form-group">
<label for="studentsSelect">Select Student</label>
<select class="form-control" id="studentsSelect">
</select>
</div>
<button type="submit" class="btn btn-primary">Select</button>
<hr />
</form>
<p id="accountAddress" class="text-center"></p>
</div>
</div>
</div>
</div>
<!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
<!-- Include all compiled plugins (below), or include individual files as needed -->
<script src="js/bootstrap.min.js"></script>
<script src="js/web3.min.js"></script>
<script src="js/truffle-contract.js"></script>
<script src="js/app.js"></script>
</body>
</html>
Localhost:3000 运行我们的客户端:
npm run dev
一直loading,这时候我们的小狐狸🦊——MetaMask就派上用场了!
我们用的是主以太坊网络,应该用Ganache定义的私有链端口:
点击Costume RPC设置http://127.0.01:7545
,重新加载,页面显示正常:
但是我们注册的账户是没钱的,无法应用合约,打开Ganache私有链中的一个账户,复制私钥🔑,在Metamask上导入新账户:
我们为Nino进行一次打分:选择Nino,系统弹出一个标签,用来确认交易。
玄学问题:这个过程可能有时候会发生错误,基本上是RPC网络连接不佳、私有链连接不畅造成的,重启端口或者更换一个账号打分即可。*
为Nino成功打分,可以看见,Select选择框和按键没有了(目前规定一个账户不能重复打分):
TA4打分是需要花钱的,因此可以看见钱变少了:
再用其他账户给学生们打分吧!
系统说明
因为RPC连接不稳定,经常会出现报错:tx的nounce不正确
,因此需要频繁地更换账户,这个问题影响了系统的实用性。另外这个系统暂时不允许同一个账户多次打分,为了确保每一次打分都可以被清晰地追溯。
参考资料
吃水不忘挖井人,在此感谢给我带来帮助的重要参考: