天天看點

手摸手用Truffle開發自己的第一個DApp

前言

簡單寫個雜貨鋪的DApp, 每個人可以把自己不用的物品挂在上面, 以獲得領取别人物品的機會, 簡單來說就是共享自己無用的物品;

效果圖, 請忽略樣式, 畢竟我是個後端
手摸手用Truffle開發自己的第一個DApp

項目源碼

環境需求

  1. MetaMask
  2. node
  3. yarn
  4. Ganache
  5. truffle (npm install -g truffle)
  6. lite-server (yarn add lite-server )

後端

初始化項目

truffle init 
npm init
           
目錄介紹
|-- Dapp
    |-- build                 // 合約編譯後自動建立
    |-- contracts             // 放置合約檔案
    |-- migrations            // 放置部署檔案
    |-- test                  // 放置測試檔案
           

編寫合約

contracts/Grocery.sol
pragma solidity ^0.5.0;
pragma experimental ABIEncoderV2;


contract Grocery {
    struct Goods {
        string name;
        string img;
        bool isClaim;
    }

    mapping(uint => address) goodsMap;  // 商品的歸屬
    Goods[] public goodsList;  // 所有商品清單
    mapping(address => uint[]) claimList;  // 領取清單
    mapping(address => int) claimNum; // 領取統計
    int maxClaim = 10;  // 最大領取數量, 可以用過添加商品來得到更多的機會

    // @notice 添加商品
    function AddGoods(string memory _name, string memory _img) public returns (uint){
        uint id = goodsList.push(Goods(_name, _img, false)) - 1;
        goodsMap[id] = msg.sender;
        claimNum[msg.sender] -= 1;
        return id;
    }

    // @notice 領取商品
    function Claim(uint _id) public {
        require(claimNum[msg.sender] <= maxClaim, "您已經領取的足夠多了");
        require(!goodsList[_id].isClaim, "該商品已經被認領");
        goodsMap[_id] = msg.sender;
        goodsList[_id].isClaim = true;
        claimList[msg.sender].push(_id);
        claimNum[msg.sender] += 1;
    }

    // @notice 擷取所有的商品
    function GetAllGoods() public view returns (Goods[] memory){
        return goodsList;
    }

    // @notice 擷取商品歸屬位址
    function GoodsOf(uint _id) public view returns (address) {
        return goodsMap[_id];
    }
}
           

代碼邏輯也比較簡單, 每個人都可以添加商品, 并且獲得領取次數, 已經被領取的商品不允許被再次領取;

編寫部署腳本

migrations/2_initial_grocery.js
const Grocery = artifacts.require("Grocery");

module.exports = function (deployer) {
    deployer.deploy(Grocery);
};
           

配置網絡

打開ganache, 啟動區塊鍊, 預設會生成10個有100個ETH的賬戶
手摸手用Truffle開發自己的第一個DApp
檢視配置端口, 預設都是7545, 當然也可以修改
手摸手用Truffle開發自己的第一個DApp
修改項目truffle-config.js 裡面的配置端口, 連通測試區塊
development: {
     host: "127.0.0.1",     // Localhost (default: none)
     port: 7545,            // Standard Ethereum port (default: none)
     network_id: "*",       // Any network (default: none)
 }
           

部署合約并進行互動

進入truffle控制台
truffle console
           

可以看到 truffle(development) 連接配接上了development環境

編譯合約
compile
           

成功後有如下輸出, 并且項目會多個build目錄, 裡面有編譯好的json合約檔案

> Compiled successfully using:
   - solc: 0.5.16+commit.9c3226ce.Emscripten.clang
           
部署合約
migrate
migrate --reset    // 如果合約修改了需要重新部署, 則需要添加reset參數
           

成功後有如下輸出, 建立了兩個合約

Summary
=======
> Total deployments:   2
> Final cost:           0.02087428 ETH

           

可以在打開的 Ganache 中看到區塊變化, 預設花費第一個帳戶的ETH

手摸手用Truffle開發自己的第一個DApp

測試

成功部署後當然要測試一下合約的準确了, 這裡可以控制台直接連接配接測試, 也可以編寫js/sol測試檔案來測試

控制台測試

let accounts = await web3.eth.getAccounts()     // 擷取目前所有的賬戶, 也就是初始化的是個位址
accounts                                       // 列印位址
let instance = await Grocery.deployed()        // 擷取合約實列
           
用第二個賬戶去添加商品添加商品
instance.AddGoods("zombie", "https://dss0.bdstatic.com/70cFvHSh_Q1YnxGkpoWK1HF6hhy/it/u=3994931373,529771369&fm=26&gp=0.jpg", {from: accounts[1]})
           

成功後返會交易詳情

{
  tx: '0x3433af229a978e5ae61ad42845041e30136bb381e2ca759415bc05e602ffe042',
  receipt: {
    transactionHash: '0x3433af229a978e5ae61ad42845041e30136bb381e2ca759415bc05e602ffe042',
    transactionIndex: 0,
    blockHash: '0xeb67216667d37474859c60ce33c13bcb8654af40b50c14ceed38bd763d673fa1',
    blockNumber: 5,
    from: '0xd1f614d6577df534f5d17d5c9aba14331515436f',
    to: '0xa916c6f1439c77942d26e9317ef401b164d96978',
    gasUsed: 171053,
    cumulativeGasUsed: 171053,
    contractAddress: null,
    logs: [],
    status: true,
    logsBloom: '0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
00000000000000000000000000000000000000',
    rawLogs: []
  },
  logs: []
}

           
用第三個賬戶取認領
instance.Claim(0, {from: accounts[2]})
           

成功後返會交易詳情

{
  tx: '0x1b264ac1497f03c33a7db0b7f89212fe4537b53801cc34a72afda4a744c02cee',
  receipt: {
    transactionHash: '0x1b264ac1497f03c33a7db0b7f89212fe4537b53801cc34a72afda4a744c02cee',
    transactionIndex: 0,
    blockHash: '0x28c3404ce1007f9b7aba3c06038216697af2afbd6c6a4035cbb59df99f97a248',
    blockNumber: 6,
    from: '0x2e097441b0828c69245dfec4ae970945acba7206',
    to: '0xa916c6f1439c77942d26e9317ef401b164d96978',
    gasUsed: 95538,
    cumulativeGasUsed: 95538,
    contractAddress: null,
    logs: [],
    status: true,
    logsBloom: '0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
00000000000000000000000000000000000000',
    rawLogs: []
  },
  logs: []
}
           
擷取商品歸屬
instance.GoodsOf(0)
           

成功後傳回位址

'0x2E097441B0828c69245DFec4ae970945acbA7206'
           
擷取所有商品清單
instance.GetAllGoods()
           

傳回

[
  [
    'zombie',
    'https://dss0.bdstatic.com/70cFvHSh_Q1YnxGkpoWK1HF6hhy/it/u=3994931373,529771369&fm=26&gp=0.jpg',
    true,
    name: 'zombie',
    img: 'https://dss0.bdstatic.com/70cFvHSh_Q1YnxGkpoWK1HF6hhy/it/u=3994931373,529771369&fm=26&gp=0.jpg',
    isClaim: true
  ]
]
           

編寫測試檔案

test/TestGrocery.sol
pragma solidity ^0.5.0;

import "truffle/Assert.sol";              // 引入的斷言
import "truffle/DeployedAddresses.sol";  // 用來擷取被測試合約的位址
import "../contracts/Grocery.sol";       // 被測試合約

contract TestGrocery {
    Grocery grocery = Grocery(DeployedAddresses.Grocery());

    //  釋出商品測試
    function testAddGoods() public {
        uint id = grocery.AddGoods("GoLang", "https://img-blog.csdnimg.cn/20190629154954578.png?x-oss-process=image/resize,m_fixed,h_64,w_64");
        uint expected = 0;
        Assert.equal(id, expected, "Goods id is not 0");
    }

    // 領取商品測試
    function testClaim() public {
        // 期望領取者的位址就是本合約位址,因為交易是由測試合約發起交易,
        address expected = address(this);
        grocery.Claim(0);
        address claimAddr = grocery.GoodsOf(0);
        Assert.equal(claimAddr, expected, "Owner of goods id 1 should be recorded.");
    }
}
           

在truffle控制台調用test指令, 傳回如下

TestGrocery
    √ testAddGoods (379ms)
    √ testClaim (598ms)


  2 passing (14s)
           

測試成功

前端

目錄介紹

|-- Dapp
    |-- src             //項目根目錄下建立src目錄,存放前端檔案
        -- index.html
        |-- js
        |-- fonts
        |-- css
    -- bs-config.json    // lite-server的配置檔案
           

配置 lite-server

安裝
yarn add lite-server
           
bs-config.json 指定 lite-server 的工作目錄
{
  "server": {
    "baseDir": ["./src", "./build/contracts"]
  }
}
           

配置啟動腳本

package.json
"scripts": {
  "dev": "lite-server"   // 添加這行, 啟動 yarn run dev 
}
           

代碼

大家直接看我的源碼吧, 前端屬實好幾年沒寫過了, 這裡是根據參考部落格的前端代碼改的, 然後js裡面的truffle-contract.js 和web3.js 是從node_modules裡面copy出來的, 因為合約裡面有一個傳回數組的, 是以得用較新版本的包; 大家主要看看

app.js裡面的代碼知道怎麼初始化調用合約就可以了;

啟動

yarn run dev
           

MetaMask 添加本地測試網

點選MetaMask右上角網絡選擇自定義Rpc加入本地測試網
手摸手用Truffle開發自己的第一個DApp
然後點選導入賬戶, 在Ganache中找個有ETH的私鑰導進去就行
手摸手用Truffle開發自己的第一個DApp
手摸手用Truffle開發自己的第一個DApp
這樣就可以用MetaMash進行添加和認領物品的确認操作了
手摸手用Truffle開發自己的第一個DApp

部署到Ropsten測試網絡

安裝

yarn add truffle-hdwallet-provider

注冊https://infura.io 擷取節點連接配接

注冊項目擷取PROJECT ID
手摸手用Truffle開發自己的第一個DApp

擷取MetaMask賬戶助記詞, 要在Ropsten網絡有ETH

賬戶設定的安全與隐私裡面可以擷取到助記詞
手摸手用Truffle開發自己的第一個DApp

增加Ropsten網絡配置

truffle-config.js 實際項目生成都有寫好的, 打開注釋就可以了
const HDWalletProvider = require('@truffle/hdwallet-provider');
const infuraKey = "infura擷取的projectId";
const mnemonic = "助記詞";

module.exports = {
    ropsten: {
    provider: () => new HDWalletProvider(mnemonic, "https://ropsten.infura.io/"+infuraKey),
    network_id: 3,       // Ropsten's id
    gas: 5500000,        // Ropsten has a lower block limit than mainnet
    confirmations: 2,    // # of confs to wait between deployments. (default: 0)
    timeoutBlocks: 200,  // # of blocks before a deployment times out  (minimum/default: 50)
    skipDryRun: true     // Skip dry run before migrations? (default: false for public nets )
    },
}
           

部署合約

truffle migrate --network ropsten --reset --compile-all

成功傳回

Summary
=======
> Total deployments:   2
> Final cost:          0.0211683 ETH

           

啟動

切換MetaMash的網絡環境為Ropsten測試網絡, 然後F5重新整理頁面就可以了, app.js裡判斷了如果有web網絡注入會優先選取, 注意! 測試網絡的區塊打包會慢一些, 不會像本地區塊一樣添加領取秒成功, 需要等區塊确認後重新整理頁面才能顯示

擴充

當然有興趣的朋友也可以在此之上多添加一些功能, 比如:
  1. 新增商品和領取通知(event);
  2. 顯示目前賬戶可領取次數;

參考文檔

https://learnblockchain.cn/docs/truffle/index.html
https://learnblockchain.cn/2018/01/12/first-dapp/#%E5%88%9B%E5%BB%BA%E7%94%A8%E6%88%B7%E6%8E%A5%E5%8F%A3%E5%92%8C%E6%99%BA%E8%83%BD%E5%90%88%E7%BA%A6%E4%BA%A4%E4%BA%92
https://www.jianshu.com/p/b7be51dd8e84
https://cloud.tencent.com/developer/article/1347300
           

繼續閱讀