第五課 以太坊開發框架Truffle從入門到實戰

筆名輝哥發表於2018-11-15

第五課 以太坊開發框架Truffle從入門到實戰

【本文目標】
通過本文的學習和時間,你將熟悉以太坊開發框架Truffle的配置和執行,並藉助Truffle完成一個智慧合約的部署。

【技術收穫】 通過本文的學習,你將掌握以下內容: 1,瞭解TRUFFLE的功能 2,瞭解TRUFFLE的安裝,配置和啟動 3,藉助TRUFFLE完成METACOIN一個智慧合約的執行 4,Testrpc,Geth環境的使用

【實操課程列表】 第一課 如何在WINDOWS環境下搭建以太坊開發環境

第二課 如何實現以太坊最簡智慧合約“Hello World”的執行

第四課 以太坊開發框架Truffle從入門到實戰

第六課 技術小白如何開發一個DAPP區塊鏈應用(以寵物商店為例)

第七課 技術小白如何在45分鐘內發行通證(TOKEN)並上線交易

第八課 如何除錯以太坊官網的智慧合約眾籌案例

【說明】未列出的課程為知識普及的非實操類課程,所有區塊鏈文章參考“區塊鏈入口”專欄。

1. TRUFFLE是什麼?

Truffle是一個世界級的開發環境,測試框架,以太坊的資源管理通道,致力於讓以太坊上的開發變得簡單,Truffle有以下:

  • 內建的智慧合約編譯,連結,部署和二進位制檔案的管理。
  • 快速開發下的自動合約測試。
  • 指令碼化的,可擴充套件的部署與釋出框架。
  • 部署到不管多少的公網或私網的網路環境管理功能
  • 使用EthPM&NPM提供的包管理,使用ERC190標準。
  • 與合約直接通訊的直接互動控制檯(寫完合約就可以命令列裡驗證了)。
  • 可配的構建流程,支援緊密整合。
  • 在Truffle環境裡支援執行外部的指令碼。 【說明】更多以太坊術語可參考此篇文章: www.jianshu.com/p/036661986…

1.1 TRUFFLE的安裝

在Ubuntu命令上視窗輸入以下命令,完成安裝:

$ npm install -g truffle

如果安裝成功,可輸入truffle version名稱,正常情況下會有版本顯示:

truffle version

環境要求

NodeJS 5.0+ Windows,Linux(推薦Ubuntu),或Mac OS X

Truffle客戶端

有許多的以太坊客戶端可以選擇。我們推薦在開發和部署時使用不同客戶端。 適用開發的客戶端

當開發基於Truffle的應用時,我們推薦使用EthereumJS TestRPC。它是一個完整的在記憶體中的區塊鏈僅僅存在於你開發的裝置上。它在執行交易時是實時返回,而不等待預設的出塊時間,這樣你可以快速驗證你新寫的程式碼,當出現錯誤時,也能即時反饋給你。它同時還是一個支援自動化測試的功能強大的客戶端。Truffle充分利用它的特性,能將測試執行時間提速近90%。

適用正式釋出的客戶端

對此有許多官方和非官方的以太坊客戶端可供選擇。最好使用TestRPC客戶端充分測試後,再使用這些客戶端。這些是完整的客戶端實現,包括挖礦,網路,塊及交易的處理,Truffle可以在不需要額外配置的情況下發布到這些客戶端。

當釋出到私有網路中

私人網路中使用了相同的技術,但卻有不同的配置。所以你可以將上面提及的客戶端來執行一個私有的網路,部署到這樣的網路也是使用同樣的方式。 【說明】作者使用TestRPCGeth (go-ethereum)這2種客戶端,他們的安裝方式參考文章:www.jianshu.com/p/683ea7d62…

2. 下載TRUFFLE MetaCoin樣例進行環境搭建實戰

2.1 MetaCoin初始化

我們假設前面的安裝和環境搭建已全部成功,此時應該可以直接使用命令truffle了,下面我們建立一個工作間truffle-workspace,然後在工作間執行:

mkdir MetaCoin
cd MetaCoin
truffle unbox metacoin
複製程式碼

原來使用truffle init,但現在它存在於unbox。

執行截圖如下:

下載樣例

unbox

Truffle 的盒子Boxs裝有很多非常實用的專案樣板,可以讓你忽略一些環境配置問題,從而可以集中與開發你自己的DApp的業務唯一性。除此之外,Truffle Boxes能夠容納其他有用的元件、Solidity合約或者庫,前後端檢視等等。所有這些都是一個完整的例項Dapp程式。都可以下載下來逐一研究,尋找適合自己公司目前業務模型的元件。

Truffle的官方Boxes地址

可以看到,現在官方盒子還不多,總共7個,有三個是關於react的,兩個是truffle自己的專案,可以下載體驗,剩下兩個是我們比較關心的,一個是metacoin,非常好的入門示例,另一個是webpack,顧名思義,它是一套比起metacoin更加完整的模板的存在。既然我們是初學,下面我們就從metacoin入手學習。

1) tutorialtoken 1] This box has all you need to get started with our Open Zeppelin (TutorialToken) tutorial. 2] truffleframework.com/boxes/tutor…

2)PET-SHOP 1] This box has all you need to get started with our Pet Shop tutorial. 2] truffleframework.com/boxes/pet-s…

3) METACOIN truffle unbox metacoin

4) ENDLESS-NAMELESS-INC/CHESHIRE(加密貓) 1] An Ethereum testnet running the CryptoKitties smart contracts An HTTP server running a minimal implementation of the CryptoKitties web API: A simple Node.js framework for seeding the development environment with realistic data and bootstraping your dApp. 2] truffleframework.com/boxes/chesh…

2.2 目錄結構及檔案解讀

進入metacoin目錄,當前目錄已經被初始化成一個新的空的以太坊工程,目錄結構如下:

contracts * ConvertLib.sol * MetaCoin.sol * Migrations.sol * .placeholder migrations * 1_initial_migration.js * 2_deploy_contracts.js test * metacoin.js * TestMetacoin.sol * .placeholder

  • truffle-config.js
  • truffle.js

初始化檔案解釋1:Migrations.sol

pragma solidity ^0.4.2;

contract Migrations {
  address public owner;
  uint public last_completed_migration;

  modifier restricted() {
    if (msg.sender == owner) _;
  }

  function Migrations() public {
    owner = msg.sender;
  }

  function setCompleted(uint completed) public restricted {
    last_completed_migration = completed;
  }

  function upgrade(address new_address) public restricted {
    Migrations upgraded = Migrations(new_address);
    upgraded.setCompleted(last_completed_migration);
  }
}

複製程式碼

上面我們學習了Solidity具體的型別語法,我們來分析一下這個檔案:

  • 它定義了一個名字為“遷移”的合約
  • 有一個任意訪問的全域性變數,儲存於storage的地址型別變數owner
  • 有一個可任意訪問的全域性變數,儲存於storage的無符號整型型別的變數last_completed_migration
  • modifier下面細說,此處略過
  • msg.sender下面細說,此處略過
  • 建構函式,初始化將傳送方賦值給owner儲存
  • 一個setCompleted賦值方法,賦值給last_completed_migration,其中該方法被宣告為restricted,下面細說,此處略過
  • upgrade方法,呼叫當前合約自己的方法,得到合約的例項upgraded,然後通過該是咧呼叫setCompleted賦值方法。
Solidity語法補充說明1:function modifier

modifier的使用方法,就看上面的Migrations合約的例子即可,它可以自動改變函式的行為,例如你可以給他預設一個條件,他會不斷檢查,一旦符合條件即可走預設分支。它可以影響當前合約以及派生合約。

pragma solidity ^0.4.11;

contract owned {
    function owned() public { owner = msg.sender; }
    address owner;
    // 這裡僅定義了一個modifier但是沒有使用,它將被子類使用,方法體在這裡“_;”,這意味著如果owner呼叫了這個函式,函式會被執行,其他人呼叫會丟擲一個異常。
    modifier onlyOwner {
        require(msg.sender == owner);
        _;
    }
}

// 通過is關鍵字來繼承一個合約類,mortal是owned的子類,也叫派生類。
contract mortal is owned {
    // 當前合約派生了owned,此方法使用了父類的onlyOwner的modifier
    // public onlyOwner, 這種寫法挺讓人困惑,下面給出了我的思考,暫理解為派生類要使用基類的modifier。
    function close() public onlyOwner {
        selfdestruct(owner);
    }
}

contract priced {
    // Modifiers可以接收引數
    modifier costs(uint price) {
        // 這裡modifier方法體是通過條件判斷,是否滿足,滿足則執行“_;”分支。
        if (msg.value >= price) {
            _;
        }
    }
}

contract Register is priced, owned {
    mapping (address => bool) registeredAddresses;
    uint price;

    // 建構函式給全域性變數price賦值。
    function Register(uint initialPrice) public { price = initialPrice; }

    // payable關鍵字重申,如果不宣告的話,函式關於以太幣交易的操作都會被拒回。
    function register() public payable costs(price) {
        registeredAddresses[msg.sender] = true;
    }

    // 此派生類也要使用基類的modifier。
    function changePrice(uint _price) public onlyOwner {
        price = _price;
    }
}

contract Mutex {
    bool locked;
    modifier noReentrancy() {
        require(!locked);
        locked = true;
        _;
        locked = false;
    }

    function f() public noReentrancy returns (uint) {
        require(msg.sender.call());
        return 7;
    }
}
複製程式碼

又延伸出來一個盲點:require關鍵字,它是錯誤判斷,提到assert就懂了,官方文件的解釋為:

require(bool condition):
throws if the condition is not met - to be used for errors in inputs or external components.
複製程式碼

總結一下modifier:

  • 宣告modifier時,特殊符號“_;”的意思有點像TODO,是一個“佔位符”,指出了你要寫的具體方法體內容的位置。
  • function close() public onlyOwner,派生類某方法想“如虎添翼”加入基類的某個modifier功能,就可以這樣寫,這行的具體意思就是:close方法也必須是owner本人執行,否則報錯!
Solidity語法補充說明2:Restricting Access

限制訪問一種針對合約的常見模式。但其實你永遠不可能限制得了任何人或電腦讀取你的交易內容或者你的合同狀態。你可以使用加密增大困難,但你的合約就是用來讀取資料的,那麼其他人也會看到。所以,其實上面的modifier onlyOwner是一個特別好的可讀性極高的限制訪問的手段。

那麼restricted關鍵字如何使用呢?

好吧,我剛剛帶著modifier的知識重新看了上面的Migrations合約的內容發現,restricted並不是關鍵字,而是modifier的方法名,在其下的想增加該modifier功能的函式中,都使用了public restricted的方式來宣告。

說到這裡,我又明白了為什麼要使用public onlyOwner這種寫法,因為public是函式可見性修飾符,onlyOwner是自定義的限制訪問的modifier方法,他們都是關於函式使用限制方面的,所以會寫在一起,可以假想一個括號將它倆括起來,他們佔一個位置,就是原來屬於public|private|internal|external的那個位置。

Solidity語法補充說明3:Special Variables and Functions

這一點很重要了,我們研究一下Solidity自身攜帶的特殊變數以及函式:

  1. block.blockhash(uint blockNumber) returns (bytes32): 返回引數區塊編號的hash值。(範圍僅限於最近256塊,還不包含當然塊)
  2. block.coinbase (address): 當前區塊礦工地址
  3. block.difficulty (uint): 當前區塊難度
  4. block.gaslimit (uint): 當前區塊的gaslimit
  5. block.number (uint): 當前區塊編號
  6. block.timestamp (uint): 當前區塊的timestamp,使用UNIX時間秒
  7. msg.data (bytes): 完整的calldata
  8. msg.gas (uint): 剩餘的gas
  9. msg.sender (address): 資訊的傳送方 (當前呼叫)
  10. msg.sig (bytes4): calldata的前四個位元組 (i.e. 函式識別符號)
  11. msg.value (uint): 訊息傳送的wei的數量
  12. now (uint): 當前區塊的timestamp (block.timestamp別名)
  13. tx.gasprice (uint): 交易的gas單價
  14. tx.origin (address): 交易傳送方地址(完全的鏈呼叫)

msg有兩個屬性,一個是msg.sender,另一個是msg.value,這兩個值可以被任何external函式呼叫,包含庫裡面的函式。

注意謹慎使用block.timestamp, now and block.blockhash,因為他們都是有可能被篡改的。

初始化檔案解釋2:MetaCoin.sol

pragma solidity ^0.4.18;

import "./ConvertLib.sol";

// 這是一個簡單的仿幣合約的例子。它並不是標準的可相容其他幣或token的合約,
// 如果你想建立一個標準相容的token,請轉到 https://github.com/ConsenSys/Tokens(TODO:一會兒我們再過去轉)

contract MetaCoin {
        mapping (address => uint) balances;// 定義了一個對映型別變數balances,key為address型別,值為無符整型,應該是用來儲存每個賬戶的餘額,可以存多個。

        event Transfer(address indexed _from, address indexed _to, uint256 _value);// Solidity語法event,TODO:見下方詳解。

        function MetaCoin() public {// 建構函式,tx.origin查查上面,找到它會返回交易傳送方的地址,也就是說合約例項建立時會預設為當前交易傳送方的餘額塞10000,單位應該是你的仿幣。
                balances[tx.origin] = 10000;
        }

        function sendCoin(address receiver, uint amount) public returns(bool sufficient) {// 函式宣告部分沒有盲點,方法名,引數列表,函式可見性,返回值型別定義。
                if (balances[msg.sender] < amount) return false;// 如果餘額不足,則返回傳送幣失敗
                balances[msg.sender] -= amount;// 否則從傳送方餘額中減去傳送值,注意Solidity也有 “-=”,“+=” 的運算子哦
                balances[receiver] += amount;// 然後在接收方的餘額中加入傳送值數量。
                Transfer(msg.sender, receiver, amount);// 使用以上event關鍵字宣告的方法
                return true;
        }

        function getBalanceInEth(address addr) public view returns(uint){// 獲取以太幣餘額
                return ConvertLib.convert(getBalance(addr),2);// 呼叫了其他合約的方法,TODO:稍後介紹ConvertLib合約時說明。
        }

        function getBalance(address addr) public view returns(uint) {// 獲取當前賬戶的仿幣餘額
                return balances[addr];
        }
}

複製程式碼
Solidity語法補充說明4:Events

Events allow the convenient usage of the EVM logging facilities, which in turn can be used to “call” JavaScript callbacks in the user interface of a dapp, which listen for these events. Events提供了日誌支援,進而可用於在使用者介面上“呼叫”dapp JavaScript回撥,監聽了這些事件。簡單來說,我們的DApp是基於web伺服器上的web3.js與EVM以太坊結點進行互動的,而智慧合約是部署在EVM以太坊結點上的。舉一個例子:

contract ExampleContract {
  // some state variables ...
  function foo(int256 _value) returns (int256) {
    // manipulate state ...
    return _value;
  }
}
複製程式碼

合約ExampleContract有個方法foo被部署在EVM的一個結點上執行了,此時使用者如果想在DApp上呼叫合約內部的這個foo方法,如何操作呢,有兩種辦法:

  1. var returnValue = exampleContract.foo.call(2);// 通過web3 的message的call來呼叫。
  2. 合約內部再宣告一個event ReturnValue(address indexed _from, int256 _value);並在foo方法內使用該event用來返回方法執行結果。

第一種辦法在方法本身比較耗時的情況下會阻塞,或者不會獲取到準確的返回值。所以採用第二種辦法:就是通過Solidity的關鍵字event。event在這裡就是一個回撥函式的概念,當函式執行結束以後(交易進塊),會通過event返回給web3,也就是DApp使用者介面相應的結果。這是以太坊一種客戶端非同步呼叫方法。關於這個回撥,要在DApp使用web3時顯示編寫:

exampleEvent.watch(function(err, result) {
  if (err) {
    console.log(err)
    return;
  }
  console.log(result.args._value)
  // 檢查合約方法是否反返回結果,若有則將結果顯示在使用者介面並且呼叫exampleEvent.stopWatching()方法停止非同步回撥監聽。
})
複製程式碼

寫Solidity最大的不同在於,我們要隨時計算好我們的gas消耗,方法的複雜度,變數型別的儲存位置(memory,storage等等)都會決定gas的消耗量。

使用event可以獲得比storage更便宜的gas消耗。

總結一下event,就是如果你的Dapp客戶端web3.js想呼叫智慧合約內部的函式,則使用event作為橋樑,它能方便執行非同步呼叫同時又節約gas消耗。

初始化檔案解釋3:ConvertLib.sol
pragma solidity ^0.4.4;

library ConvertLib{
        function convert(uint amount,uint conversionRate) public pure returns (uint convertedAmount)
        {
                return amount * conversionRate;
        }
}
複製程式碼

與MetaCoin智慧合約不同的是,ConvertLib是由library宣告的一個庫,它只有一個方法,就是返回給定的兩個無符整數值相乘的結果。返回到上面的MetaCoin中該庫的使用位置去分析,即可知道,MetaCoin的仿幣的價格是以太幣的一倍,所以MetaCoin是以以太幣為標杆,通過智慧合約釋出的一個token,仿幣。

這似乎就可以很好地解決我在《以太坊RPC機制與API例項》文章中需要釋出三倍以太幣的token的需求了,而我們完全不必更改以太坊原始碼,但那篇文章通過這個需求的路線研究了以太坊的Go原始碼也算功不可沒。

初始化檔案解釋4:1_initial_migration.js

var Migrations = artifacts.require("./Migrations.sol");

module.exports = function(deployer) {
  deployer.deploy(Migrations);
};
複製程式碼

這個js檔案是nodejs的寫法,看上去它的作用就是部署了上面的Migrations智慧合約檔案。

初始化檔案解釋5:2_deploy_contracts.js

var ConvertLib = artifacts.require("./ConvertLib.sol");
var MetaCoin = artifacts.require("./MetaCoin.sol");

module.exports = function(deployer) {
  deployer.deploy(ConvertLib);
  deployer.link(ConvertLib, MetaCoin);
  deployer.deploy(MetaCoin);
};

複製程式碼

這個檔案是meatcoin智慧合約的部署檔案,裡面約定了部署順序,依賴關係。這裡我們看到了MetaCoin智慧合約是要依賴於庫ConvertLib的,所以要先部署ConvertLib,然後link他們,再部署MetaCoin,這部分js的寫法可以參照官方文件DEPLOYER API,主要就是介紹了一下deploy、link以及then三個方法的詳細用法,不難這裡不再贅述。

初始化檔案解釋6:truffle-config.js, truffle.js

module.exports = {
  // See <http://truffleframework.com/docs/advanced/configuration>
  // to customize your Truffle configuration!
};
module.exports = {
  // See <http://truffleframework.com/docs/advanced/configuration>
  // to customize your Truffle configuration!
};

複製程式碼

這兩個檔案也都是nodejs,他們都是配置檔案,可能作用域不同,目前它倆是完全相同的(因為啥也沒有)。我們去它推薦的網站看一看。給出了一個例子:

module.exports = {
  networks: {
    development: {
      host: "127.0.0.1",
      port: 8545,
      network_id: "*" // Match any network id
    }
  }
};
複製程式碼

這個例子展示了該配置檔案可以配置網路環境,暫先到這,以後遇上了針對該配置檔案進行研究。

初始化檔案解釋7:.placeholder

This is a placeholder file to ensure the parent directory in the git repository. Feel free to remove.

翻譯過來就是:placeholder檔案是用來保證在git庫中父級目錄的,可以刪除。

初始化檔案解釋8:metacoin.js

和下面的檔案一樣,他們的功能都是用來做單元測試的,truffle在編譯期間會自動執行這些測試指令碼。當前檔案為js版本,模擬使用者在DApp客戶端使用者介面操作的情形。

var MetaCoin = artifacts.require("./MetaCoin.sol"); // 這與1_initial_migration.js檔案的頭是一樣的,引入了一個智慧合約檔案。

contract('MetaCoin', function(accounts) {
  it("should put 10000 MetaCoin in the first account", function() {
    return MetaCoin.deployed().then(function(instance) {
      return instance.getBalance.call(accounts[0]);
    }).then(function(balance) {
      assert.equal(balance.valueOf(), 10000, "10000 wasn't in the first account");
    });
  });
  it("should call a function that depends on a linked library", function() {
    var meta;
    var metaCoinBalance;
    var metaCoinEthBalance;

    return MetaCoin.deployed().then(function(instance) {
      meta = instance;
      return meta.getBalance.call(accounts[0]);
    }).then(function(outCoinBalance) {
      metaCoinBalance = outCoinBalance.toNumber();
      return meta.getBalanceInEth.call(accounts[0]);
    }).then(function(outCoinBalanceEth) {
      metaCoinEthBalance = outCoinBalanceEth.toNumber();
    }).then(function() {
      assert.equal(metaCoinEthBalance, 2 * metaCoinBalance, "Library function returned unexpected function, linkage may be broken");
    });
  });
  it("should send coin correctly", function() {
    var meta;

    // Get initial balances of first and second account.
    var account_one = accounts[0];
    var account_two = accounts[1];

    var account_one_starting_balance;
    var account_two_starting_balance;
    var account_one_ending_balance;
    var account_two_ending_balance;

    var amount = 10;

    return MetaCoin.deployed().then(function(instance) {
      meta = instance;
      return meta.getBalance.call(account_one);
    }).then(function(balance) {
      account_one_starting_balance = balance.toNumber();
      return meta.getBalance.call(account_two);
    }).then(function(balance) {
      account_two_starting_balance = balance.toNumber();
      return meta.sendCoin(account_two, amount, {from: account_one});
    }).then(function() {
      return meta.getBalance.call(account_one);
    }).then(function(balance) {
      account_one_ending_balance = balance.toNumber();
      return meta.getBalance.call(account_two);
    }).then(function(balance) {
      account_two_ending_balance = balance.toNumber();

      assert.equal(account_one_ending_balance, account_one_starting_balance - amount, "Amount wasn't correctly taken from the sender");
      assert.equal(account_two_ending_balance, account_two_starting_balance + amount, "Amount wasn't correctly sent to the receiver");
    });
  });
});

複製程式碼

我們來分析一波這個truffle metacoin js版本的單元測試:

  1. 直接函式contract走起,第一個引數為智慧合約名字,第二個引數為匿名內部函式
  2. 匿名函式傳入了當前賬戶地址,函式體是單元測試集
  3. 每個單元測試是由關鍵字it函式來做,第一個引數傳入單元測試的comments,第二個引數傳入一個無參匿名函式
  4. 進到無參匿名函式的函式體內,就是正式的單元測試內容,可以定義自己的成員屬性,通過呼叫truffle內部元件自動部署合約逐一測試,使用成員屬性接收返回值,最後使用關鍵字assert來判斷是否符合預期。具體業務不詳細展開,可根據自己業務內容隨意更改。

這是官方文件,詳細說明如何使用JS來編寫智慧合約的單元測試

初始化檔案解釋9:TestMetacoin.sol

好下面來看看Solidity智慧合約版本的單元測試。一般來講,這種檔案的命名規則是Test加待測智慧合約的名字拼串組成。

pragma solidity ^0.4.2;

import "truffle/Assert.sol";
import "truffle/DeployedAddresses.sol";
import "../contracts/MetaCoin.sol";

contract TestMetacoin {

  function testInitialBalanceUsingDeployedContract() public {
    MetaCoin meta = MetaCoin(DeployedAddresses.MetaCoin());

    uint expected = 10000;

    Assert.equal(meta.getBalance(tx.origin), expected, "Owner should have 10000 MetaCoin initially");
  }

  function testInitialBalanceWithNewMetaCoin() public {
    MetaCoin meta = new MetaCoin();

    uint expected = 10000;

    Assert.equal(meta.getBalance(tx.origin), expected, "Owner should have 10000 MetaCoin initially");
  }

}

複製程式碼

繼續分析:

  • 首先import了truffle的幾個類庫,用來支援我們接下來的測試內容。然後import了待測智慧合約。
  • 建立單元測試智慧合約,根據合約不同方法定義對應的test測試方法。
  • 方法體內部去呼叫待測智慧合約的方法,傳參接收返回值,然後使用關鍵字assert判斷是否符合預期。

這是官方文件,詳細說明如何使用Solidity來編寫智慧合約的單元測試

2.3 編譯合約

鍵入

truffle compile

輸出情況:

輸出結果

根據編譯輸出的路徑地址./build/contracts,我們去檢視一下

產生檔案列表

可以看到原來所在在contracts目錄下的智慧合約檔案(有合約contract,有庫library)均被編譯成了json檔案。

這些json檔案就是truffle用來部署合約的編譯檔案。 ##2.4 配置以太坊本地環境 truffle.js是truffle的配置檔案,啟動好以太坊本地結點以後,我們需要讓truffle去識別它並使用它,這就需要在truffle.js中配置相關屬性:

module.exports = {
  networks: {
    development: {
      host: "127.0.0.1",
      port: 8545,
      network_id: "*" // Match any network id
    }
  }
};
複製程式碼

【說明】如果不啟動TestRPC,直接執行部署合約的話,會有以下錯誤提示:

無網路

2.5 啟動本地以太坊客戶端結點

啟動適合開發的RPC客戶端

啟動之前安裝好的EthereumJS RPC客戶端。

testrpc

【說明】一定要啟動一個新的客戶端執行testrpc命令,可以觀察到預設賬戶和私鑰資訊。

本地客戶端

2.6 部署合約

移植(migrate),對這裡叫移植,但下面我們仍使用“部署”這個詞,truffle中部署的命令為:

truffle migrate

輸出結果截圖如下:

部署智慧合約的輸出結果

檢視testrpc的輸出視窗,可以看到這筆交易和花費的區塊:

image.png

2.7 測試合約

我們知道在執行編譯時會自動執行這些單元測試,如果有一個測試未通過則會中斷編譯過程。而在開發階段,我們也可以自己使用命令來測試。

truffle test

沒有報錯就說明通過了,綠條“5 passing(2s)”,有報錯就會列印在下方。

輸出截圖1
輸出截圖2

3. 用Truffle框架執行一個“Hello World!”智慧合約

3.1 建立工程目錄

返回父級目錄,建立一個資料夾HelloWorld,來做為你的工程根目錄。

mkdir HelloWorld

輸入結果:

建立並進入該目錄

3.2 初始化框架

在工作目錄HelloWorld目錄下,執行truffle初始化動作:

truffle init

輸出截圖:

初始化成功
採用SFTP下載檔案到本地,可檢視目錄結構:

│  truffle-config.js
│  truffle.js
│  
├─contracts
│      Migrations.sol
│      
├─migrations
│      1_initial_migration.js
│      
└─test
複製程式碼

目錄結構簡單說明如下:

contract/ - Truffle預設的合約檔案存放地址。 migrations/ - 存放釋出指令碼檔案 test/ - 用來測試應用和合約的測試檔案 truffle.js - Truffle的配置檔案

3.3 新建新合約

在./contract目錄下建立一個自己的合約檔案Greeter.sol。

pragma solidity ^0.4.17;

contract Greeter         
{
    address creator;     
    string greeting;     

    function Greeter(string _greeting) public   
    {
        creator = msg.sender;
        greeting = _greeting;
    }
    

    function greet() public constant returns (string)           
    {
        return greeting;
    }
    
    function setGreeting(string _newgreeting) public
    {
        greeting = _newgreeting;
    }
    
     /**********
     Standard kill() function to recover funds 
     **********/
    
    function kill()public
    { 
        if (msg.sender == creator)
            suicide(creator);  // kills this contract and sends remaining funds back to creator
    }

}
複製程式碼

3.4 新建釋出指令碼

在./migrations/目錄下新建一個檔案:2_deploy_contracts.js,增加發布程式碼。

var Greeter = artifacts.require("./Greeter.sol");

module.exports = function(deployer) {
  deployer.deploy(Greeter,"Hello, World!");//"引數在第二個變數攜帶"
};
複製程式碼

3.5 編譯

進入到工程根目錄./HelloWorld目錄下,進行編譯:

truffle compile

輸出截圖如下:

編譯成功截圖

3.6 啟動你的客戶端

如果之前沒有啟動RPC客戶端的話,則需要啟動之前安裝好的EthereumJS RPC客戶端。如果已啟動的則忽略此步。

$ testrpc

3.7 部署合約(migrate)

執行部署命令(truffle migrate)提示出錯。

truffle migrate

錯誤截圖輸出:

部署失敗,提示網路未配置
修改檔案./HelloWorld/truffle.js檔案,增加網路配置:

module.exports = {
  // See <http://truffleframework.com/docs/advanced/configuration>
  // to customize your Truffle configuration!
     networks: {
        development: {
            host: "localhost",
            port: 8545,
            network_id: "*" // 匹配任何network id
         }
    }
};
複製程式碼

重新執行編譯命令,重新執行部署命令(truffle migrate),則執行正確。對應Greeter的智慧合約地址為“0x7d62724f397a99613b84923a1166d683de2db680”

部署成功

3.8 TRUFFLE測試環境執行合約

Truffle提供了一種更加簡單的方式,通過互動式控制檯來與你的那些準備好的合約進行互動。 truffle console 一個基本的互動控制檯,可以連線任何EVM客戶端。如果你已經有了自己的ganache或者geth等EVM的本地環境,那麼就可以使用truffle console來互動,所以如果你已經有一個現成的小組共享的開發用EVM,那麼使用這個沒錯。 truffle develop 一個互動控制檯,啟動時會自動生成一個開發用區塊鏈環境(其實我認為它與ganache就是一個底層實現機制,都是預設生成10個賬戶)。如果你沒有自己的EVM環境的話,直接使用truffle develop非常方便。

truffle console

輸入Greeter智慧合約命令,顯示列印出一個json結構,展示了它的各種屬性內容。

檢視Greeter結構
根據你的Greeter智慧合約地址,執行Greeter智慧合約命令:
hello,world智慧合約執行成功

3.9 GETH正式環境執行合約

###啟動GETH環境 本節假設GETH環境已安裝好了。如果還沒有安裝的同學,可參考文章《第一課 如何在WINDOWS環境下搭建以太坊開發環境》(www.jianshu.com/p/683ea7d62…

然後在IDE內部開啟一個terminal,啟動GETH的EVM環境。

geth --datadir testNet3 --dev --rpc console

截圖1
截圖2

GETH 中是通過abi來註冊合約物件的。 首先我們找到./build/contracts/Greeter.json中的abi的value:

"abi": [
    {
      "inputs": [
        {
          "name": "_greeting",
          "type": "string"
        }
      ],
      "payable": false,
      "stateMutability": "nonpayable",
      "type": "constructor"
    },
    {
      "constant": true,
      "inputs": [],
      "name": "greet",
      "outputs": [
        {
          "name": "",
          "type": "string"
        }
      ],
      "payable": false,
      "stateMutability": "view",
      "type": "function"
    },
    {
      "constant": false,
      "inputs": [
        {
          "name": "_newgreeting",
          "type": "string"
        }
      ],
      "name": "setGreeting",
      "outputs": [],
      "payable": false,
      "stateMutability": "nonpayable",
      "type": "function"
    },
    {
      "constant": false,
      "inputs": [],
      "name": "kill",
      "outputs": [],
      "payable": false,
      "stateMutability": "nonpayable",
      "type": "function"
    }
  ],
複製程式碼

通過json壓縮成一行得到 var abi = [{"inputs": [{"name": "_greeting","type": "string"}],"payable": false,"stateMutability": "nonpayable","type": "constructor"},{"constant": true,"inputs": [],"name": "greet","outputs": [{"name": "","type": "string"}],"payable": false,"stateMutability": "view","type": "function"},{"constant": false,"inputs": [{"name": "_newgreeting","type": "string"}],"name": "setGreeting","outputs": [],"payable": false,"stateMutability": "nonpayable","type": "function"},{"constant": false,"inputs": [],"name": "kill","outputs": [],"payable": false,"stateMutability": "nonpayable","type": "function"}];

重新部署智慧合約到Geth環境

啟動一個新的命令視窗,到

cd /usr/work/HelloWorld truffle migrate

成功部署輸出截圖:

智慧合約部署成功
獲得Greeter的地址為 0xb52bb3ce336f71a14345c78e5b2f8e63685e3f92

切換到GETH環境下,利用api和智慧合約地址(你自己Greeter智慧合約的地址哦)註冊合約物件。

var abi = [{"inputs": [{"name": "_greeting","type": "string"}],"payable": false,"stateMutability": "nonpayable","type": "constructor"},{"constant": true,"inputs": [],"name": "greet","outputs": [{"name": "","type": "string"}],"payable": false,"stateMutability": "view","type": "function"},{"constant": false,"inputs": [{"name": "_newgreeting","type": "string"}],"name": "setGreeting","outputs": [],"payable": false,"stateMutability": "nonpayable","type": "function"},{"constant": false,"inputs": [],"name": "kill","outputs": [],"payable": false,"stateMutability": "nonpayable","type": "function"}];
var HelloWorld = eth.contract(abi).at('0xb52bb3ce336f71a14345c78e5b2f8e63685e3f92')
HelloWorld.greet()
複製程式碼

輸出截圖顯示成功:

又是成功

4. 總結及參考

本文站在巨人的肩膀上,完成了以太坊開發框架Truffle從入門到實戰的演示。對巨人的文章表示感謝: 1,Solidity的Truffle框架實戰(手把手) 2, 【精解】開發一個智慧合約 3,官網參考: truffleframework.com/docs/ 4, 官網GITHUB的程式碼: github.com/trufflesuit…

知識對接服務: 輝哥和歐陽哥哥在知識星球開通了區塊鏈入門專欄,用於存放簡書區塊鏈入門專欄文章的工程原始碼等內容,並建立專項微信群用於技術交流,歡迎加入。

第五課 以太坊開發框架Truffle從入門到實戰

相關文章