1. 前言
DApp(Decentralized Application): 后臺(tái)運(yùn)行在去中心化的點(diǎn)對(duì)點(diǎn)網(wǎng)絡(luò),與此相對(duì)的app,后臺(tái)是跑在一個(gè)中心server上的。以太坊上的DApp,就是通過(guò)智能合約,和區(qū)塊鏈進(jìn)行交互。
2. 環(huán)境準(zhǔn)備
在搭建完ethereum私有鏈之后,就可以進(jìn)行開發(fā)啦,想想就很雞凍~不過(guò),還是得準(zhǔn)備一下開發(fā)環(huán)境先。
3. 項(xiàng)目介紹
一個(gè)寵物店,有16只寵物,現(xiàn)在開發(fā)一個(gè)去中心化應(yīng)用,讓大家來(lái)領(lǐng)養(yǎng)寵物。
在truffle box中,已經(jīng)提供了pet-shop的網(wǎng)站部分的代碼,我們只需要編寫合約及交互部分。項(xiàng)目UI先睹為快:

3.1 創(chuàng)建項(xiàng)目目錄
mkdir pet-shop-tutorial
cd pet-shop-tutorial
3.2 使用truffle unbox 創(chuàng)建項(xiàng)目
truffle unbox pet-shop
這一步可能需要花點(diǎn)時(shí)間,因?yàn)樗枰ハ螺dnode_modules, 請(qǐng)耐心等待...
結(jié)果:
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 項(xiàng)目結(jié)構(gòu)
- contracts 智能合約存放文件夾
- migrations 處理智能合約的部署
- test 測(cè)試用例
- truffle.js 部署時(shí)候的配置文件
3.4 編寫智能合約
contracts/Adoption.
pragma solidity ^0.4.17;
contract Adoption {
address[16] public adopters; // 地址數(shù)組,分別對(duì)應(yīng)寵物0-15的領(lǐng)養(yǎng)人的地址
// 領(lǐng)養(yǎng)寵物
function adopt(uint petId) public returns (uint) {
require(petId >= 0 && petId <= 15); // 確保id在數(shù)組長(zhǎng)度內(nèi)
adopters[petId] = msg.sender; // 保存領(lǐng)養(yǎng)者的地址
return petId;
}
// 返回領(lǐng)養(yǎng)者
function getAdopters() public view returns (address[16]) {
return adopters;
}
}
這里用來(lái)編寫Ethereum智能合約的語(yǔ)言叫Solidity, 暫時(shí)不用細(xì)究具體的語(yǔ)法,這里的例子也很簡(jiǎn)單明了,繼續(xù)往下走~
3.5 編譯部署
3.5.1 編譯
把Solidity代碼編譯為EVM字節(jié)碼,在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
這里出現(xiàn)一些warning, 但是無(wú)關(guān)緊要。這里的warinig的意思就是類似在java里面寫方法沒(méi)寫public修飾符,但是編譯的時(shí)候默認(rèn)會(huì)當(dāng)成public的編譯。
編譯完成后,會(huì)多出來(lái)一個(gè)build文件夾,里面的contracts就是編譯好的代碼,待會(huì)部署就是依賴這些文件。
3.5.2 部署前的準(zhǔn)備
在migrations目錄下面,已經(jīng)存在一個(gè)文件1_initial_migration.js。如果沒(méi)有這個(gè)文件的話,也可以通過(guò)truffle init命令來(lái)生成。這個(gè)文件的作用,就是部署Migrations.sol這個(gè)合約。關(guān)于這個(gè)合約,truffle官方的介紹是:
You must deploy this contract inside your first migration in order to take advantage of the Migrations feature.
既然是必須,那就照著做咯。
下面要部署我們的Adoption.sol,我們也要寫一個(gè)專門部署這個(gè)合約的部署文件出來(lái),名字叫2_deploy_adoption.js咯。這里的起名有點(diǎn)學(xué)問(wèn),truffle會(huì)按照你起的的部署文件名字順序部署。
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);
};
然后,在項(xiàng)目根目錄有個(gè)叫做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
}
}
};
在這個(gè)文件,指定了你將要部署你的合約到哪個(gè)地方。按照之前搭建環(huán)境,我把它的端口從7545改成8545,networkid從*改成1024。gas代表愿意出多少單位的gas來(lái)部署你的合約,default是4712388。我把它改成了我的私鏈創(chuàng)世塊的gasLimit(如在這里不指定gas, 而你的創(chuàng)世塊中的gasLimit又比default值小,到時(shí)候部署會(huì)報(bào)一個(gè)錯(cuò):exceeds block gas limit)其他的具體的配置參數(shù),還可以參考truffle官方介紹。
最后, 進(jìn)入到geth控制臺(tái)創(chuàng)建一個(gè)賬戶, 如果有賬戶了,可以跳過(guò)。
personal.newAccount()
連續(xù)兩次輸入密碼后,一個(gè)賬戶就已經(jīng)創(chuàng)建好啦,控制臺(tái)打印出來(lái)的就是你的賬戶地址,記住密碼不要忘了~再啰嗦多點(diǎn),剛才創(chuàng)建的賬號(hào)信息,已經(jīng)存在了節(jié)點(diǎn)數(shù)據(jù)庫(kù)的一個(gè)叫做keystore的文件夾下面,文件名類似UTC--2018-02-08T16-35-23.044654600Z--9d7578d663e204c90c2b419c05a02046104446f2, 是一個(gè)時(shí)間戳+地址的格式。交易的時(shí)候,Ethereum會(huì)用你剛才的密碼和這個(gè)賬戶文件結(jié)合做數(shù)字簽名,然后打包進(jìn)交易信息廣播出去。接到交易信息的節(jié)點(diǎn),會(huì)驗(yàn)證這個(gè)交易的合法性,然后挖礦保存交易~
說(shuō)到這里,還得看看你的賬戶有沒(méi)有余額。部署智能合約是要給錢的,這個(gè)錢在Ethereum里面叫做gas,gas是從以太幣轉(zhuǎn)換得來(lái)的。
[圖片上傳失敗...(image-573dfe-1522567562814)]
所以說(shuō)到底,就是要求你的賬戶得有以太幣Ether。在geth控制臺(tái)查看一下你的賬戶余額:
web3.fromWei(eth.getBalance(eth.coinbase),"Ether")
這里打印出來(lái)的余額,單位是“Ether”, 以太幣。如果想看gas有多少,直接eth.getBalance(eth.coinbase)就可以了。還有如果你的賬戶沒(méi)錢,那就去挖一下礦吧。
miner.start()
如果你是第一次挖礦,要等一段時(shí)間初始化好才能出礦。差不多的話,就可以停止挖了。
miner.stop()
至此,部署準(zhǔn)備工作完成。
3.5.3 部署
部署前,確保你的私鏈環(huán)境已經(jīng)起來(lái)。然后,還得保證你的私鏈上有節(jié)點(diǎn)在挖礦。因?yàn)椴渴鹬悄芎霞s,其實(shí)也是發(fā)送交易,得有礦工把你的交易保存到具體區(qū)塊并確認(rèn)才算真正部署成功。接著,在項(xiàng)目根目錄下面執(zhí)行命名:
truffle migrate
有可能,你會(huì)看到下面部署失敗的日志:
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在一段時(shí)間后會(huì)自動(dòng)鎖住賬戶。所以解決的辦法是,進(jìn)入geth交互模式,去解鎖你的默認(rèn)賬號(hào)eth.coinbase,因?yàn)椴渴鸬臅r(shí)候,默認(rèn)是用這個(gè)賬戶去部署的,除非你在truffle.js指定一個(gè)賬戶去部署,那你就去解鎖相對(duì)應(yīng)的賬戶~
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...
部署成功。這時(shí)候,我一般會(huì)miner.stop()一下, 因?yàn)樗接墟湹囊蕴珟挪辉诙?,夠用就好。而且挖礦越多,后面越難挖。因?yàn)槊總€(gè)block的difficulty逐漸增大, 那么挖礦需要算出的nonce就越大,就意味著出礦時(shí)間要相對(duì)長(zhǎng),不利于后面開發(fā)調(diào)試。具體每個(gè)block的信息,可以通過(guò)eth.getBlock(i)來(lái)查看。
3.6 測(cè)試
3.6.1 編寫測(cè)試
truffle已經(jīng)提供好測(cè)試框架給我們啦。在test文件夾新建:TestAdoption.sol
pragma solidity ^0.4.17;
import "truffle/Assert.sol"; // 引入的斷言
import "truffle/DeployedAddresses.sol"; // 用來(lái)獲取被測(cè)試合約的地址
import "../contracts/Adoption.sol"; // 被測(cè)試合約
contract TestAdoption {
Adoption adoption = Adoption(DeployedAddresses.Adoption());
// 領(lǐng)養(yǎng)測(cè)試用例
function testUserCanAdoptPet() public {
uint returnedId = adoption.adopt(8);
uint expected = 8;
Assert.equal(returnedId, expected, "Adoption of pet ID 8 should be recorded.");
}
// 寵物所有者測(cè)試用例
function testGetAdopterAddressByPetId() public {
// 期望領(lǐng)養(yǎng)者的地址就是本合約地址,因?yàn)榻灰资怯蓽y(cè)試合約發(fā)起交易,
address expected = this;
address adopter = adoption.adopters(8);
Assert.equal(adopter, expected, "Owner of pet ID 8 should be recorded.");
}
// 測(cè)試所有領(lǐng)養(yǎng)者
function testGetAdopterAddressByPetIdInArray() public {
// 領(lǐng)養(yǎng)者的地址就是本合約地址
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 運(yùn)行測(cè)試
前提:賬戶解鎖,有礦工挖礦
truffle test --network development
--network development指定用truffle.js的develeopment配置u運(yùn)行測(cè)試。
結(jié)果:
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)
這里不得記錄一個(gè)坑, 最初跑測(cè)試的時(shí)候遇到的:
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)
有人提過(guò)相同的issue, 但是這個(gè)對(duì)我還是沒(méi)有幫助,問(wèn)題沒(méi)得到解決,但問(wèn)題基本鎖定是gas搞的鬼。直到我看到這篇文章, 我把初始化創(chuàng)世塊的genesis.json中的gasLimit改成一個(gè)比較大的值(從0x2fefd9改成0x8000000)并重新搭建一條私鏈后,問(wèn)題就神奇地解決了, 感動(dòng)得淚流滿面哇~
3.7 UI
當(dāng)我們的智能合約ready后,就可以開始實(shí)現(xiàn)UI部分了。在truffle框架中,前端的代碼寫在src下面。在這個(gè)pet-shop中,開箱已經(jīng)有部分可用的代碼了,現(xiàn)在我們只需編寫和智能合約交互的部分。這里要用到的是web3.js, Ethereum的JavaScript API, 通過(guò)web3.js, 我們可以和已經(jīng)部署好的智能合約進(jìn)行交互。
下面修改src/js/app.js。
3.7.1 初始化web3
找到initWeb3這個(gè)function,實(shí)現(xiàn)如下:
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();
}
代碼中優(yōu)先使用Mist或 MetaMask為瀏覽器注入的web3實(shí)例,如果沒(méi)有則從本地環(huán)境創(chuàng)建一個(gè)。這里的8545就是本地節(jié)點(diǎn)監(jiān)聽的rpc端口。
3.7.2 實(shí)例化合約
找到initContract, 實(shí)現(xiàn)如下:
initContract: function() {
// 加載Adoption.json,保存了Adoption的ABI(接口說(shuō)明)信息及部署后的網(wǎng)絡(luò)(地址)信息,它在編譯合約的時(shí)候生成ABI,在部署的時(shí)候追加網(wǎng)絡(luò)信息
$.getJSON('Adoption.json', function(data) {
// 用Adoption.json數(shù)據(jù)創(chuàng)建一個(gè)可交互的TruffleContract合約實(shí)例。
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 標(biāo)記領(lǐng)養(yǎng)狀態(tài)
修改markAdopted方法:
markAdopted: function(adopters, account) {
var adoptionInstance;
App.contracts.Adoption.deployed().then(function(instance) {
adoptionInstance = instance;
// 調(diào)用合約的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 處理領(lǐng)養(yǎng)事件
修改handleAdopt 方法:
handleAdopt: function(event) {
event.preventDefault();
var petId = parseInt($(event.target).data('id'));
var adoptionInstance;
// 獲取用戶賬號(hào)
web3.eth.getAccounts(function(error, accounts) {
if (error) {
console.log(error);
}
var account = accounts[0];// 用第一個(gè)賬號(hào)領(lǐng)養(yǎng)
App.contracts.Adoption.deployed().then(function(instance) {
adoptionInstance = instance;
// 發(fā)送交易領(lǐng)養(yǎng)寵物
return adoptionInstance.adopt(petId, {from: account});
}).then(function(result) {
return App.markAdopted();
}).catch(function(err) {
console.log(err.message);
});
});
}
3.8 運(yùn)行APP
在pet-shop目錄下,運(yùn)行npm run dev, 會(huì)啟動(dòng)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個(gè)寵物正在等著你去領(lǐng)養(yǎng)。點(diǎn)擊領(lǐng)養(yǎng),會(huì)發(fā)現(xiàn)按鈕的文字變成success并不可再點(diǎn)擊。如果領(lǐng)養(yǎng)不成功,很有可能是你的賬戶鎖住了,此時(shí)你需要去解鎖你對(duì)應(yīng)的賬戶。還有一個(gè)原因會(huì)造成領(lǐng)養(yǎng)不成功,那就是私鏈上沒(méi)有節(jié)點(diǎn)在挖礦,交易無(wú)法保存。此時(shí)miner.start()就可以了~

寵物領(lǐng)養(yǎng)的數(shù)據(jù)已經(jīng)保存至區(qū)塊鏈,即使你的節(jié)點(diǎn)重啟,領(lǐng)養(yǎng)的數(shù)據(jù)還是會(huì)在,就跟歷史一樣,發(fā)生了就是發(fā)生了,無(wú)法篡改,而且已經(jīng)同步到區(qū)塊鏈上的各個(gè)其他節(jié)點(diǎn)。
如果你修改了合約重新編譯部署,那之前的領(lǐng)養(yǎng)數(shù)據(jù)就...也還是在區(qū)塊鏈上保存著的,只是新的合約無(wú)法再獲取到之前舊合約的數(shù)據(jù)。當(dāng)然,如果你保存了舊部署之后的ABI, 也就是build目錄下面的json文件,用于replace掉現(xiàn)在的build文件夾下面的json文件,那么你就可以穿梭回舊版本的pet-shop了,從UI可以發(fā)現(xiàn),領(lǐng)養(yǎng)的數(shù)據(jù)還是在的。
3.9 結(jié)束
參考文檔: