--- tags: it 鐵人賽 30 天, Web 3, ethereum --- # 從以太坊白皮書理解 web 3 概念 - Day17 ## Learn Solidity - Day 9 - Deploy Smart Contracts with Truffle 今天將會介紹如何透過 Truffle 發佈 Smart Contract 讀者可以一起使用 [Lession 10: Deploy Smart Contracts with Truffle](https://cryptozombies.io/en/lesson/10) 跟著本文一起實作 首先要介紹開發與編譯 Smart Contract 工具- Truffle ## Truffle Truffle 是一套目前蠻受歡迎的開發 Smart Contract 工具 主要特性有以下: * 編譯 Smart Contrat 方便 * 自動產生 ABI * 整合 Smart Contract 的測試工具,支援 Mocha 與 Chai 等測試常用 assertion tool * 支援多個鏈的網路 - 可以透過 Truffle 發佈到 Rinkeby,Ethereum,或是 Loom 安裝需求,需要先安裝 node.js 還有 npm 以下將會使用全域安裝的方式來安裝 Truffle 。 ### 執行安裝 1 建立一個資料夾 CryptoZombies ,並且使用 cd 進入這個資料夾 2 透過 npm 或是 yarn 全域安裝 Truffle ```shell= npm i truffle -g ``` ```shell= yarn global add truffle ``` ## 初始化 Truffle 專案 透過 truffle init 就可以初始化一個 Truffle 專案 檔案結構如下 ```javascript= ├── contracts ├── Migrations.sol ├── migrations ├── 1_initial_migration.js └── test truffle-config.js truffle.j ``` ![](https://i.imgur.com/045Wjuo.png) ## Truffle 預設的資料夾結構 * contracts: 這個資料夾就是用來存放所有 Smart Contract 的內容 * migrations: 這個資料夾會存放 javascript 檔案用來作為 deploy Smart Contrat 的程序 * test: 用來存放撰寫測試 Smart Contract 的邏輯 * truffle.js 與 truffle-config.js: 用來設定佈署到哪個鏈網路的設定檔案Truffle 會需要兩個設定檔是因為在 Windows 作業系統中, truffle.js 與 truffle.exe 檔會放在同個資料夾可能會產生檔名衝突。所以如果在Window 會建議使用 truffle-config.js 作為預設設定檔,而刪除 truffle.js ## truffle-hdwallet-provider 在這個課程,將會使用 infura 來發佈 Smart Contract 這樣就不用自己架設 Ethereum 節點或是錢包。 然而為了安全性,Infura 服務並不會幫忙管理簽章私鑰,代表我們必須要額外使用錢包來做這件事情。 為了方便做這件事情,將會使用 truffle-hdwallet-provider 這個套件來處理做簽章的事情。 ### 初始化專案 1. 執行 truffle init 2. 安裝 truffle-hdwallet-provider ```shell= npm install truffle-hdwallet-provider ``` 或是 ```shell= yarn add truffle-hdwallet-provider ``` ## Compile source code EVM 無法直接解讀 Solidity source code 所以需要把 Solidity source code 編議程 bytecode EVM 才能讀取並執行 bytecode 的格式大致如下: ```shell= "0x60806040526010600155600154600a0a6002556201518060035566038d7ea4c6800060085560006009556046600a55336000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1..." ``` ### Solidity Compiler Solidity Compiler 負責編譯 solidity source code 為 bytecode 並且會做一些語法的上建議 比如說以下的一個 function ```solidity= function add(uint16 a, uint16 b) internal returns (uint16) { uint16 c = a + b; assert(c >= a); return c; } ``` 當 Solidity Compiler 編譯之後就會出現以下警告 ```solidity= safemath.sol:110:11: Warning: Function state mutability can be restricted to pure function add(uint16 a, uint16 b) internal returns (uint16) { ^ (Relevant source part starts here and spans across multiple lines). ``` 這個警告是因為這個 function 不需要對區塊鏈做讀取只是單純做計算因為會建議宣告為 pure function 透過宣告為 pure 可以節省呼叫時所需的 gas ### 編譯 Contract 的語法 ```shell= truffle compile ``` ## Migration 在正式發佈 Smart Contract 到正式鏈之前 通常都會先在本地端對 Contract 做測試。 可以使用 Ganache 來當作本地節點 為了要發佈到正式鏈,會需要建立 migration migration 是用來協助 Truffle 發佈 Smart Contract 的 javascript 檔案。 備註: truffle init 會建立一個特別的 Contract 叫作 Migrations.sol。 Migration.sol 是用來紀錄每次 Contract 的修改。 因此,整個修改 Contract 的歷史會被紀錄到鏈上,所以不會發佈兩次相同的 Contract 在上面。 ### 建立一個新的 Migration 使用 truffle init 會建立了一個叫作 ./contracts/1_initial_migration.js 內容如下: ```javascript= var Migrations = artifacts.require("./Migrations.sol"); module.exports = function(deployer) { deployer.deploy(Migrations); }; ``` 檔案內容是從 Migration.sol 讀出修改的 Contract 然後透過 deployer 發佈 如果要新增新的發佈 則需要建立一個新的檔案 ./contracts/2_crypto_zombies.js 內容類似於上一個檔案,只是引入不同的 Contract ## 實作 1. 修改 ./contracts/2_crypto_zombies.js 如下 ```javascript= var CryptoZombies = artifacts.require("./CryptoZombies.sol"); module.exports = function(deployer) { deployer.deploy(CryptoZombies); }; ``` ## 設定檔 為了發佈到測試鏈 需要對設定檔做一些調整 ### Ethereum Test Networks 乙太鏈具有許多公開的測試鏈可以用來測試 Smart Contract 在這裡將會使用 Rinkeby 來做測試 ### truffle.js 設定內容 預設的 Truffle 設定內容如下 ```javascript= /* * NB: since truffle-hdwallet-provider 0.0.5 you must wrap HDWallet providers in a * function when declaring them. Failure to do so will cause commands to hang. ex: * * mainnet: { * provider: function() { * return new HDWalletProvider(mnemonic, 'https://mainnet.infura.io/<infura-key>') * }, * network_id: '1', * gas: 4500000, * gasPrice: 10000000000, * }, */ ``` 因此需要加入 Rinkeby 的設定 ### Truffle HD Wallet Provider 為了建立簽章,使用了 truffle-hdwallet-provider 這個套件 為了要能夠再測試環境使用,必須要加入以下修改到設定檔 ```javascript= var HDWalletProvider = require("truffle-hdwallet-provider"); ``` ### 設定 Rinkeby 修改 network 參數值如下 ```javascript= networks: { // Configuration for mainnet mainnet: { provider: function () { // Setting the provider with the Infura Mainnet address and Token return new HDWalletProvider(mnemonic, "https://mainnet.infura.io/v3/YOUR_TOKEN") }, network_id: "1" }, // Configuration for rinkeby network rinkeby: { // Special function to setup the provider provider: function () { // Setting the provider with the Infura Rinkeby address and Token return new HDWalletProvider(mnemonic, "https://rinkeby.infura.io/v3/YOUR_TOKEN") }, // Network id is 4 for Rinkeby network_id: 4 } ``` 最後修改值如下 ```javascript= / Initialize HDWalletProvider const HDWalletProvider = require("truffle-hdwallet-provider"); // Set your own mnemonic here const mnemonic = "YOUR_MNEMONIC"; // Module exports to make this configuration available to Truffle itself module.exports = { // Object with configuration for each network networks: { // Configuration for mainnet mainnet: { provider: function () { // Setting the provider with the Infura Mainnet address and Token return new HDWalletProvider(mnemonic, "https://mainnet.infura.io/v3/YOUR_TOKEN") }, network_id: "1" }, // Configuration for rinkeby network rinkeby: { // Special function to setup the provider provider: function () { // Setting the provider with the Infura Rinkeby address and Token return new HDWalletProvider(mnemonic, "https://rinkeby.infura.io/v3/YOUR_TOKEN") }, // Network id is 4 for Rinkeby network_id: "4" } } }; ``` ## Deploy Smart Contract Truffle 透過 migration 來做發佈 Smart Contract ### Migrations Migration 本身就是一個用來做發佈 Contract 的 Javascript 檔案 ### Get some Ether 要發佈 Smart Contract 會需要消耗一些 Ether 要在測試鏈取得 Ether 可以透過一個叫作 faucet 的服務來取得 而 Rinkeby 的 faucet 就在 [Authenticated Faucet](https://faucet.rinkeby.io/) 只要把 account address 填入就可以得到 Rinkeby 鏈上的 Ether ### 實作 1. 透過以下指令發佈 Smart Contract 到 Rinkeby ```shell= truffle migrate --network rinkeby ``` 備註: truffle deploy 只是 truffle migrate 的別名 而如果要發佈到正式鏈只需要把 network 換成 mainnet ## 使用 Loom 鏈來做發佈 ### Loom 鏈 在 Loom 鏈上做交易不需要任何 gas,並且比正式鏈交易速度快很多 在 Loom 鏈上比較適合做非金融的 Dapp 而 Deploy 到 Loom 的步驟與 Deploy 到 Rinkeby 一樣 只需要修改 network 設定 還有使用 loom-truffle-provider ### loom-truffle-provider 可以使用 Truffle 來發佈 Smart Contract 到 Loom 需要使用 loom-truffle-provider 來與 Loom 節點溝通 ### 實作 1. 安裝 loom-truffle-provider ```shell= npm i loom-truffle-provider ``` or ```shell= yarn add loom-truffle-provider ``` ## Deploy 到 Loom 測試鏈 要發佈 Smart Contract 在 Loom 測試鏈 首先需要建立 Loom private 可以透過 [Loom 設定](https://loomx.io/developers/en/basic-install-all.html#purpose) 來安裝 Loom cli 然後透過 loom cli 建立 private key 如下 ```shell= $./loom genkey -a public_key -k private_key local address: 0x42F401139048AB106c9e25DCae0Cf4b1Df985c39 local address base64: QvQBE5BIqxBsniXcrgz0sd+YXDk= $cat private_key /i0Qi8e/E+kVEIJLRPV5HJgn0sQBVi88EQw/Mq4ePFD1JGV1Nm14dA446BsPe3ajte3t/tpj7HaHDL84+Ce4Dg== ``` ### 更新 truffle.js 設定檔 新增 LoomTruffleProvider 如下 ```javascript= const LoomTruffleProvider = require('loom-truffle-provider'); ``` 加入 loom 的 network 設定如下 ```javascript= loom_testnet: { provider: function() { const privateKey = 'YOUR_PRIVATE_KEY' const chainId = 'extdev-plasma-us1'; const writeUrl = 'http://extdev-plasma-us1.dappchains.com:80/rpc'; const readUrl = 'http://extdev-plasma-us1.dappchains.com:80/query'; return new LoomTruffleProvider(chainId, writeUrl, readUrl, privateKey); }, network_id: '9545242630824' } ``` 更新後如下 ```javascript= // Initialize HDWalletProvider const HDWalletProvider = require("truffle-hdwallet-provider"); // 1. Initialize LoomTruffleProvider const LoomTruffleProvider = require('loom-truffle-provider'); // Set your own mnemonic here const mnemonic = "YOUR_MNEMONIC"; // Module exports to make this configuration available to Truffle itself module.exports = { // Object with configuration for each network networks: { // Configuration for mainnet mainnet: { provider: function () { // Setting the provider with the Infura Rinkeby address and Token return new HDWalletProvider(mnemonic, "https://mainnet.infura.io/v3/YOUR_TOKEN") }, network_id: "1" }, // Configuration for rinkeby network rinkeby: { // Special function to setup the provider provider: function () { // Setting the provider with the Infura Rinkeby address and Token return new HDWalletProvider(mnemonic, "https://rinkeby.infura.io/v3/YOUR_TOKEN") }, // Network id is 4 for Rinkeby network_id: 4 }, // 2. Put here the configuration for loom_dapp_chain loom_testnet: { provider: function() { const privateKey = 'YOUR_PRIVATE_KEY' const chainId = 'extdev-plasma-us1'; const writeUrl = 'http://extdev-plasma-us1.dappchains.com:80/rpc'; const readUrl = 'http://extdev-plasma-us1.dappchains.com:80/query'; return new LoomTruffleProvider(chainId, writeUrl, readUrl, privateKey); }, network_id: '9545242630824' } } }; ``` 然後就可以透過以下指令發佈 Contract 到 Loom testnet 了 ```shell= truffle migrate --network loom_testnet ``` ## Deploy 到 Loom 正式鏈 流程如下 * 建立 private key * 把正式鏈網路設定加入 truffle.js * 透過 truffle 指令發佈 ## 建立 private key ```shell= ./loom genkey -a mainnet_public_key -k mainnet_private_key local address: 0x07419790A773Cc6a2840f1c092240922B61eC778 local address base64: B0GXkKdzzGooQPHAkiQJIrYex3g= ``` 在 LoomTruffleProvider 引入 ```javascript= function getLoomProviderWithPrivateKey (privateKeyPath, chainId, writeUrl, readUrl) { const privateKey = readFileSync(privateKeyPath, 'utf-8'); return new LoomTruffleProvider(chainId, writeUrl, readUrl, privateKey); } ``` ## 設定正式鏈網路 ```javascript= basechain: { provider: function() { const chainId = 'default'; const writeUrl = 'http://basechain.dappchains.com/rpc'; const readUrl = 'http://basechain.dappchains.com/query'; return new LoomTruffleProvider(chainId, writeUrl, readUrl, privateKey); const privateKeyPath = path.join(__dirname, 'mainnet_private_key'); const loomTruffleProvider = getLoomProviderWithPrivateKey(privateKeyPath, chainId, writeUrl, readUrl); return loomTruffleProvider; }, network_id: '*' } ``` 最後 truffle.js 就會如下 ```javascript= // Initialize HDWalletProvider const HDWalletProvider = require("truffle-hdwallet-provider"); const { readFileSync } = require('fs') const path = require('path') const { join } = require('path') // Set your own mnemonic here const mnemonic = "YOUR_MNEMONIC"; function getLoomProviderWithPrivateKey (privateKeyPath, chainId, writeUrl, readUrl) { const privateKey = readFileSync(privateKeyPath, 'utf-8'); return new LoomTruffleProvider(chainId, writeUrl, readUrl, privateKey); } // Module exports to make this configuration available to Truffle itself module.exports = { // Object with configuration for each network networks: { // Configuration for mainnet mainnet: { provider: function () { // Setting the provider with the Infura Rinkeby address and Token return new HDWalletProvider(mnemonic, "https://mainnet.infura.io/v3/YOUR_TOKEN") }, network_id: "1" }, // Configuration for rinkeby network rinkeby: { // Special function to setup the provider provider: function () { // Setting the provider with the Infura Rinkeby address and Token return new HDWalletProvider(mnemonic, "https://rinkeby.infura.io/v3/YOUR_TOKEN") }, // Network id is 4 for Rinkeby network_id: 4 }, basechain: { provider: function() { const chainId = 'default'; const writeUrl = 'http://basechain.dappchains.com/rpc'; const readUrl = 'http://basechain.dappchains.com/query'; return new LoomTruffleProvider(chainId, writeUrl, readUrl, privateKey); const privateKeyPath = path.join(__dirname, 'mainnet_private_key'); const loomTruffleProvider = getLoomProviderWithPrivateKey(privateKeyPath, chainId, writeUrl, readUrl); return loomTruffleProvider; }, network_id: '*' } } }; ``` 然後就可以透過以下指令發佈到 Loom 正式鏈了 ```shell= truffle migrate --network basechain ```