以太坊平台上的Hello World DApp

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();
  }

代码中优先使用MistMetaMask为浏览器注入的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 结束

参考文档:

http://truffleframework.com/tutorials/pet-shop

https://xiaozhuanlan.com/topic/4875690231

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容