1. 前言
DApp(Decentralized Application): 后台运行在去中心化的点对点网络,与此相对的app,后台是跑在一个中心server上的。以太坊上的DApp,就是通过智能合约,和区块链进行交互。
2. 环境准备
在搭建完ethereum私有链之后,就可以进行开发啦,想想就很鸡冻~不过,还是得准备一下开发环境先。
3. 项目介绍
一个宠物店,有16只宠物,现在开发一个去中心化应用,让大家来领养宠物。
在truffle box中,已经提供了pet-shop的网站部分的代码,我们只需要编写合约及交互部分。项目UI先睹为快:
3.1 创建项目目录
mkdir pet-shop-tutorial
cd pet-shop-tutorial
3.2 使用truffle unbox 创建项目
truffle unbox pet-shop
这一步可能需要花点时间,因为它需要去下载node_modules, 请耐心等待...
结果:
Downloading...
Unpacking...
Setting up...
Unbox successful. Sweet!
Commands:
Compile: truffle compile
Migrate: truffle migrate
Test contracts: truffle test
Run dev server: npm run dev
3.3 项目结构
- contracts 智能合约存放文件夹
- migrations 处理智能合约的部署
- test 测试用例
- truffle.js 部署时候的配置文件
3.4 编写智能合约
contracts/Adoption.
pragma solidity ^0.4.17;
contract Adoption {
address[16] public adopters; // 地址数组,分别对应宠物0-15的领养人的地址
// 领养宠物
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;
}
}
这里用来编写Ethereum智能合约的语言叫Solidity, 暂时不用细究具体的语法,这里的例子也很简单明了,继续往下走~
3.5 编译部署
3.5.1 编译
把Solidity代码编译为EVM字节码,在pet-shop目录下面:
truffle compile
输出:
Compiling .\contracts\Adoption.sol...
Compiling .\contracts\Migrations.sol...
Compilation warnings encountered:
/D/githome/blockchain/pet-shop/contracts/Migrations.sol:11:3: Warning: No visibility specified. Defaulting to "public".
function Migrations() {
^
Spanning multiple lines.
,/D/githome/blockchain/pet-shop/contracts/Migrations.sol:15:3: Warning: No visibility specified. Defaulting to "public".
function setCompleted(uint completed) restricted {
^
Spanning multiple lines.
,/D/githome/blockchain/pet-shop/contracts/Migrations.sol:19:3: Warning: No visibility specified. Defaulting to "public".
function upgrade(address new_address) restricted {
^
Spanning multiple lines.
Writing artifacts to .\build\contracts
这里出现一些warning, 但是无关紧要。这里的warinig的意思就是类似在java里面写方法没写public修饰符,但是编译的时候默认会当成public的编译。
编译完成后,会多出来一个build文件夹,里面的contracts就是编译好的代码,待会部署就是依赖这些文件。
3.5.2 部署前的准备
在migrations目录下面,已经存在一个文件1_initial_migration.js。如果没有这个文件的话,也可以通过truffle init
命令来生成。这个文件的作用,就是部署Migrations.sol这个合约。关于这个合约,truffle官方的介绍是:
You must deploy this contract inside your first migration in order to take advantage of the Migrations feature.
既然是必须,那就照着做咯。
下面要部署我们的Adoption.sol,我们也要写一个专门部署这个合约的部署文件出来,名字叫2_deploy_adoption.js咯。这里的起名有点学问,truffle会按照你起的的部署文件名字顺序部署。
From here, you can create new migrations with increasing numbered prefixes to deploy other contracts and perform further deployment steps.
在migrations目录下面,新建2_deploy_adoption.js:
var Adoption = artifacts.require("Adoption");
module.exports = function(deployer) {
deployer.deploy(Adoption);
};
然后,在项目根目录有个叫做truffle.js的部署配置文件:
module.exports = {
// See <http://truffleframework.com/docs/advanced/configuration>
// for more about customizing your Truffle configuration!
networks: {
development: {
host: "127.0.0.1",
port: 8545,
network_id: "1024",
gas: 3141593
}
}
};
在这个文件,指定了你将要部署你的合约到哪个地方。按照之前搭建环境,我把它的端口从7545改成8545,networkid从*改成1024。gas代表愿意出多少单位的gas来部署你的合约,default是4712388。我把它改成了我的私链创世块的gasLimit(如在这里不指定gas, 而你的创世块中的gasLimit又比default值小,到时候部署会报一个错:exceeds block gas limit)其他的具体的配置参数,还可以参考truffle官方介绍。
最后, 进入到geth控制台创建一个账户, 如果有账户了,可以跳过。
personal.newAccount()
连续两次输入密码后,一个账户就已经创建好啦,控制台打印出来的就是你的账户地址,记住密码不要忘了~再啰嗦多点,刚才创建的账号信息,已经存在了节点数据库的一个叫做keystore的文件夹下面,文件名类似UTC--2018-02-08T16-35-23.044654600Z--9d7578d663e204c90c2b419c05a02046104446f2
, 是一个时间戳+地址的格式。交易的时候,Ethereum会用你刚才的密码和这个账户文件结合做数字签名,然后打包进交易信息广播出去。接到交易信息的节点,会验证这个交易的合法性,然后挖矿保存交易~
说到这里,还得看看你的账户有没有余额。部署智能合约是要给钱的,这个钱在Ethereum里面叫做gas,gas是从以太币转换得来的。
[图片上传失败...(image-573dfe-1522567562814)]
所以说到底,就是要求你的账户得有以太币Ether。在geth控制台查看一下你的账户余额:
web3.fromWei(eth.getBalance(eth.coinbase),"Ether")
这里打印出来的余额,单位是“Ether”, 以太币。如果想看gas有多少,直接eth.getBalance(eth.coinbase)
就可以了。还有如果你的账户没钱,那就去挖一下矿吧。
miner.start()
如果你是第一次挖矿,要等一段时间初始化好才能出矿。差不多的话,就可以停止挖了。
miner.stop()
至此,部署准备工作完成。
3.5.3 部署
部署前,确保你的私链环境已经起来。然后,还得保证你的私链上有节点在挖矿。因为部署智能合约,其实也是发送交易,得有矿工把你的交易保存到具体区块并确认才算真正部署成功。接着,在项目根目录下面执行命名:
truffle migrate
有可能,你会看到下面部署失败的日志:
Using network 'development'.
Running migration: 1_initial_migration.js
Deploying Migrations...
... undefined
Error encountered, bailing. Network state unknown. Review successful transactions manually.
Error: authentication needed: password or unlock
为了安全性,Ethereum在一段时间后会自动锁住账户。所以解决的办法是,进入geth交互模式,去解锁你的默认账号eth.coinbase
,因为部署的时候,默认是用这个账户去部署的,除非你在truffle.js指定一个账户去部署,那你就去解锁相对应的账户~
personal.unlockAccount(eth.coinbase)
OK,再敲一遍部署的命令。
Using network 'development'.
Running migration: 1_initial_migration.js
Deploying Migrations...
... 0xbf625c89f59a08341ed9ed6df0fa5401fa0789689edd8bfc9f3148430c3bb1b4
Migrations: 0xbda1a6c2e10478dff136eeac357391ff43777554
Saving successful migration to network...
... 0xe16b7e83871dd81765c30f3a6e8987a16aab20fa635534a719600b65f2a33485
Saving artifacts...
Running migration: 2_deploy_contract.js
Deploying Adoption...
... 0x2e5e196e78713c2699689b1664cc5fb6e52a730ccd2b65d28db56d456a2cb487
Adoption: 0xaf5df9828eea7b6ea8e5f614e1e93ce3346b4e37
Saving successful migration to network...
... 0x81b036154d713852c59c7f0d183e23272cd753b201749d713166ae692035b799
Saving artifacts...
部署成功。这时候,我一般会miner.stop()
一下, 因为私有链的以太币不在多,够用就好。而且挖矿越多,后面越难挖。因为每个block的difficulty逐渐增大, 那么挖矿需要算出的nonce就越大,就意味着出矿时间要相对长,不利于后面开发调试。具体每个block的信息,可以通过eth.getBlock(i)
来查看。
3.6 测试
3.6.1 编写测试
truffle已经提供好测试框架给我们啦。在test文件夹新建:TestAdoption.sol
pragma solidity ^0.4.17;
import "truffle/Assert.sol"; // 引入的断言
import "truffle/DeployedAddresses.sol"; // 用来获取被测试合约的地址
import "../contracts/Adoption.sol"; // 被测试合约
contract TestAdoption {
Adoption adoption = Adoption(DeployedAddresses.Adoption());
// 领养测试用例
function testUserCanAdoptPet() public {
uint returnedId = adoption.adopt(8);
uint expected = 8;
Assert.equal(returnedId, expected, "Adoption of pet ID 8 should be recorded.");
}
// 宠物所有者测试用例
function testGetAdopterAddressByPetId() public {
// 期望领养者的地址就是本合约地址,因为交易是由测试合约发起交易,
address expected = this;
address adopter = adoption.adopters(8);
Assert.equal(adopter, expected, "Owner of pet ID 8 should be recorded.");
}
// 测试所有领养者
function testGetAdopterAddressByPetIdInArray() public {
// 领养者的地址就是本合约地址
address expected = this;
address[16] memory adopters = adoption.getAdopters();
Assert.equal(adopters[8], expected, "Owner of pet ID 8 should be recorded.");
}
}
3.6.2 运行测试
前提:账户解锁,有矿工挖矿
truffle test --network development
--network development指定用truffle.js的develeopment配置u运行测试。
结果:
Using network 'development'.
Compiling .\contracts\Adoption.sol...
Compiling .\test\TestAdoption.sol...
Compiling truffle/Assert.sol...
Compiling truffle/DeployedAddresses.sol...
TestAdoption
√ testUserCanAdoptPet (3017ms)
√ testGetAdopterAddressByPetId (6017ms)
√ testGetAdopterAddressByPetIdInArray (1006ms)
3 passing (17s)
这里不得记录一个坑, 最初跑测试的时候遇到的:
TestAdoption
1) "before all" hook: prepare suite
0 passing (4s)
1 failing
1) TestAdoption "before all" hook: prepare suite:
Error: The contract code couldn't be stored, please check your gas amount.
at Object.callback (C:\Users\LIRI7\AppData\Roaming\npm\node_modules\truffle\build\webpack:\~\web3\lib\web3\contract.js:147:1)
at C:\Users\LIRI7\AppData\Roaming\npm\node_modules\truffle\build\webpack:\~\web3\lib\web3\method.js:142:1
at C:\Users\LIRI7\AppData\Roaming\npm\node_modules\truffle\build\webpack:\~\web3\lib\web3\requestmanager.js:89:1
at C:\Users\LIRI7\AppData\Roaming\npm\node_modules\truffle\build\webpack:\~\truffle-provider\wrapper.js:134:1
at XMLHttpRequest.request.onreadystatechange (C:\Users\LIRI7\AppData\Roaming\npm\node_modules\truffle\build\webpack:\~\web3\lib\web3\httpprovider.js:128:1)
at XMLHttpRequestEventTarget.dispatchEvent (C:\Users\LIRI7\AppData\Roaming\npm\node_modules\truffle\build\webpack:\~\xhr2\lib\xhr2.js:64:1)
at XMLHttpRequest._setReadyState (C:\Users\LIRI7\AppData\Roaming\npm\node_modules\truffle\build\webpack:\~\xhr2\lib\xhr2.js:354:1)
at XMLHttpRequest._onHttpResponseEnd (C:\Users\LIRI7\AppData\Roaming\npm\node_modules\truffle\build\webpack:\~\xhr2\lib\xhr2.js:509:1)
at IncomingMessage.<anonymous> (C:\Users\LIRI7\AppData\Roaming\npm\node_modules\truffle\build\webpack:\~\xhr2\lib\xhr2.js:469:1)
at endReadableNT (_stream_readable.js:1056:12)
at _combinedTickCallback (internal/process/next_tick.js:138:11)
at process._tickCallback (internal/process/next_tick.js:180:9)
有人提过相同的issue, 但是这个对我还是没有帮助,问题没得到解决,但问题基本锁定是gas搞的鬼。直到我看到这篇文章, 我把初始化创世块的genesis.json中的gasLimit改成一个比较大的值(从0x2fefd9
改成0x8000000
)并重新搭建一条私链后,问题就神奇地解决了, 感动得泪流满面哇~
3.7 UI
当我们的智能合约ready后,就可以开始实现UI部分了。在truffle框架中,前端的代码写在src下面。在这个pet-shop中,开箱已经有部分可用的代码了,现在我们只需编写和智能合约交互的部分。这里要用到的是web3.js, Ethereum的JavaScript API, 通过web3.js, 我们可以和已经部署好的智能合约进行交互。
下面修改src/js/app.js。
3.7.1 初始化web3
找到initWeb3这个function,实现如下:
initWeb3: function() {
// Is there an injected web3 instance?
if (typeof web3 !== 'undefined') {
App.web3Provider = web3.currentProvider;
} else {
// If no injected web3 instance is detected, fall back to Ganache
App.web3Provider = new Web3.providers.HttpProvider('http://localhost:8545');
}
web3 = new Web3(App.web3Provider);
return App.initContract();
}
代码中优先使用Mist或 MetaMask为浏览器注入的web3实例,如果没有则从本地环境创建一个。这里的8545就是本地节点监听的rpc端口。
3.7.2 实例化合约
找到initContract, 实现如下:
initContract: function() {
// 加载Adoption.json,保存了Adoption的ABI(接口说明)信息及部署后的网络(地址)信息,它在编译合约的时候生成ABI,在部署的时候追加网络信息
$.getJSON('Adoption.json', function(data) {
// 用Adoption.json数据创建一个可交互的TruffleContract合约实例。
var AdoptionArtifact = data;
App.contracts.Adoption = TruffleContract(AdoptionArtifact);
// Set the provider for our contract
App.contracts.Adoption.setProvider(App.web3Provider);
// Use our contract to retrieve and mark the adopted pets
return App.markAdopted();
});
return App.bindEvents();
}
3.7.3 标记领养状态
修改markAdopted方法:
markAdopted: function(adopters, account) {
var adoptionInstance;
App.contracts.Adoption.deployed().then(function(instance) {
adoptionInstance = instance;
// 调用合约的getAdopters(), 用call读取信息不用消耗gas
return adoptionInstance.getAdopters.call();
}).then(function(adopters) {
for (i = 0; i < adopters.length; i++) {
if (adopters[i] !== '0x0000000000000000000000000000000000000000') {
$('.panel-pet').eq(i).find('button').text('Success').attr('disabled', true);
}
}
}).catch(function(err) {
console.log(err.message);
});
}
3.7.4 处理领养事件
修改handleAdopt 方法:
handleAdopt: function(event) {
event.preventDefault();
var petId = parseInt($(event.target).data('id'));
var adoptionInstance;
// 获取用户账号
web3.eth.getAccounts(function(error, accounts) {
if (error) {
console.log(error);
}
var account = accounts[0];// 用第一个账号领养
App.contracts.Adoption.deployed().then(function(instance) {
adoptionInstance = instance;
// 发送交易领养宠物
return adoptionInstance.adopt(petId, {from: account});
}).then(function(result) {
return App.markAdopted();
}).catch(function(err) {
console.log(err.message);
});
});
}
3.8 运行APP
在pet-shop目录下,运行npm run dev
, 会启动lite-server
> lite-server
** browser-sync config **
{ injectChanges: false,
files: [ './**/*.{html,htm,css,js}' ],
watchOptions: { ignored: 'node_modules' },
server:
{ baseDir: [ './src', './build/contracts' ],
middleware: [ [Function], [Function] ] } }
[Browsersync] Access URLs:
-------------------------------------
Local: http://localhost:3003
External: http://10.222.49.22:3003
-------------------------------------
UI: http://localhost:3004
UI External: http://10.222.49.22:3004
-------------------------------------
[Browsersync] Serving files from: ./src
[Browsersync] Serving files from: ./build/contracts
[Browsersync] Watching files...
浏览器打开http://localhost:3003, 可以看到16个宠物正在等着你去领养。点击领养,会发现按钮的文字变成success并不可再点击。如果领养不成功,很有可能是你的账户锁住了,此时你需要去解锁你对应的账户。还有一个原因会造成领养不成功,那就是私链上没有节点在挖矿,交易无法保存。此时miner.start()
就可以了~
宠物领养的数据已经保存至区块链,即使你的节点重启,领养的数据还是会在,就跟历史一样,发生了就是发生了,无法篡改,而且已经同步到区块链上的各个其他节点。
如果你修改了合约重新编译部署,那之前的领养数据就...也还是在区块链上保存着的,只是新的合约无法再获取到之前旧合约的数据。当然,如果你保存了旧部署之后的ABI, 也就是build目录下面的json文件,用于replace掉现在的build文件夹下面的json文件,那么你就可以穿梭回旧版本的pet-shop了,从UI可以发现,领养的数据还是在的。
3.9 结束
参考文档: