iT邦幫忙

2022 iThome 鐵人賽

DAY 14
0
Web 3

從以太坊白皮書理解 web 3 概念系列 第 15

從以太坊白皮書理解 web 3 概念 - Day14

  • 分享至 

  • xImage
  •  

從以太坊白皮書理解 web 3 概念 - Day14

Learn Solidity - Day 6 App Front-ends & Web3.js

前面都在講關於 Contract 的部份

今天將透過 Lession 6 - App Front-ends & Web3.js 來學習網頁前端可以透過 Web3.js 與 Contract 互動的邏輯

什麼是 Web3.js

Ethereum 網路是透過 Ethereum 節點所建立起來的。

當想要使用 Smart Contract 上的功能時

必須要與 Ethereum 節點溝通,並且傳遞以下資訊給節點:

  1. Contract 的地址
  2. 想要呼叫的 function 名稱
  3. 要傳給 function 帶入的參數

而Ethereum 節點會接收 JSON-RPC 格式與前端做互動。

如下

// Yeah... Good luck writing all your function calls this way!
// Scroll right ==>
{"jsonrpc":"2.0","method":"eth_sendTransaction","params":[{"from":"0xb60e8dd61c5d32be8058bb8eb970870f07233155","to":"0xd46e8dd67c5d32be8058bb8eb970870f07244567","gas":"0x76c0","gasPrice":"0x9184e72a000","value":"0x9184e72a","data":"0xd46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f072445675"}],"id":1}

然而,這樣的格式是在不好讀取

因此, Web3.js 這個 library 就可以讓我們使用比較好讀的格式來轉寫與 Contract 互動的邏輯

如下:

CryptoZombies.methods.createRandomZombie("Vitalik Nakamoto ?")
  .send({ from: "0xb60e8dd61c5d32be8058bb8eb970870f07233155", gas: "3000000" })

使用方式

透過套件管理器下載 web3 函式庫到 js 代碼內

// Using NPM
npm install web3

// Using Yarn
yarn add web3

// Using Bower
bower install web3

// ...etc.

然後在要引用的地方 透過 script tag 引入

<script language="javascript" type="text/javascript" src="web3.min.js"></script>

新增 Web3 project 如下

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <title>CryptoZombies front-end</title>
    <script language="javascript" type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
    <script language="javascript" type="text/javascript" src="web3.min.js"></script>
  </head>
  <body>

  </body>
</html>

Web3 Providers

如前所述,前端的應用需要透過 Web3 來與 Ethereum 的節點溝通

Web3 Provider 就是一個元件用來建立與 Ethereum 的節點連線

在建立 Web3 Provider 元件時,需要把 Ethereum 的節點 URI當作參數帶入

Ethereum 的節點可以自己建立或是使用節點服務像 Infura

Infura

Infura 是一個提供 Ethereum 節點的服務,並且具有快取機制可以讓大量讀取變快,此外透過 Infura api 來讀取是免費的。

使用範例如下

var web3 = new Web3(new Web3.providers.WebsocketProvider("wss://mainnet.infura.io/ws"));

Metamask

在區塊鏈中,使用者需要自行保存私鑰與處理交易簽章

而保存私鑰與處理交易簽章的系統就是所謂 Wallet

Metamask 是一個使用 chrome extension 技術製作的 Wallet

Metamask 可以用來產生簽章要做的私鑰並且保存,還有處理交易簽章

並且 Metamask 預設使用 Infura 作為 Web3 Provider

因此,如果要開發前端 Dapp 是一個方便的工具

然而,私鑰在區塊鏈是一個很重要代表身分的文件

所以放在 Metamask 這類軟體錢包有其風險性

因此在使用時,也要特別小心

過去也有假冒 Metamask 錢包的盜取密鑰攻擊。

使用 Metamask 的 Web3 Provider

因為 Metamask 是 Chrome Extension

Metamask 的 Web3 Provider 可以直接注入 web3 這個 js 痊域物件。而前端 app 可以透過檢查 web3.currentProvider 來對 Web3 做 provider 初始化設定

範例如下

window.addEventListener('load', function() {

  // Checking if Web3 has been injected by the browser (Mist/MetaMask)
  if (typeof web3 !== 'undefined') {
    // Use Mist/MetaMask's provider
    web3js = new Web3(web3.currentProvider);
  } else {
    // Handle the case where the user doesn't have web3. Probably
    // show them a message telling them to install Metamask in
    // order to use our app.
  }

  // Now you can start your app & access web3js freely:
  startApp();

});

加入 web3 provider 設定

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <title>CryptoZombies front-end</title>
    <script language="javascript" type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
    <script language="javascript" type="text/javascript" src="web3.min.js"></script>
  </head>
  <body>

    <script>
      // Start here
      window.addEventListener('load', function () {
        // Checking if Web3 has been injected by the browser (Mist/MetaMask)
        if (typeof web3 !== 'undefined') {
          // Use Mist/MetaMask's provider
          web3js = new Web3(web3.currentProvider);
        } else {
          // Handle the case where the user doesn't have web3. Probably
          // show them a message telling them to install Metamask in
          // order to use our app.
        }
        // Now you can start your app & access web3js freely:
        startApp()
      });  
    </script>
  </body>
</html>

新增與 Contract 互動的部份

要與 Contract 做互動需要以下兩個資訊

  1. Contract Address

在把 Contract 發佈於鏈上之後,會產生一個 Contract Address,用來讓使用 Contract 透過位置與其互動。

  1. Contract ABI

ABI 代表 Application Binary Interface。是一種把 Contract 溝通介面以二進位的表達式。

當把 Contract 發佈到鏈上, Contract 會自動產生出 ABI。

Web3.js 會透過這個表達式與 Contract 做溝通。

與 Contract 互動範例

// Instantiate myContract
var myContract = new web3js.eth.Contract(myABI, myContractAddress);

把互動邏輯放到前端

  1. 在 head tag 內 加入 cryptozombies_abi.js
  2. 在 script tag 第一行宣告 var cryptoZombies
  3. 建立 startApp function
  4. 在 startApp function 內宣告 var cryptoZombiesAddress 設定值為 deploy 之後的位址
  5. 設定 cryptoZombies = web3js.eth.Contract(cryptoZombiesABI, cryptoZombiesAddress);
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <title>CryptoZombies front-end</title>
    <script language="javascript" type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
    <script language="javascript" type="text/javascript" src="web3.min.js"></script>
    <script language="javascript" type="text/javascript" src="cryptozombies_abi.js"></script>
  </head>
  <body>

    <script>
      // 2. Start code here
      var cryptoZombies;
      function startApp() {
        var cryptoZombiesAddress = "YOUR_CONTRACT_ADDRESS";
        cryptoZombies = new web3js.eth.Contract(cryptoZombiesABI, cryptoZombiesAddress);
      }
      window.addEventListener('load', function() {

        // Checking if Web3 has been injected by the browser (Mist/MetaMask)
        if (typeof web3 !== 'undefined') {
          // Use Mist/MetaMask's provider
          web3js = new Web3(web3.currentProvider);
        } else {
          // Handle the case where the user doesn't have Metamask installed
          // Probably show them a message prompting them to install Metamask
        }

        // Now you can start your app & access web3 freely:
        startApp()

      })
    </script>
  </body>
</html>

實作呼叫 Contract 邏輯

Web3.js 提供了兩個 method 可以來呼教 Contract 的功能

  1. Call

call 是用來呼叫 view 與 pure function 。這類呼叫只是讀取值,不會影響到鏈上的資料。

假設要呼叫 myContract 中的 myMethod 並且帶入參數 123
其呼叫方式就會如下:

myContract.methods.myMethod(123).call()
  1. Send

send 是用來產生交易資料並且會改變鏈上資料狀態。所以當只是 view 或是 pure 就不需要使用 send。

備註 當使用 send 來發送交易將需會使用者付 gas 來處理交易,而錢包將會跳出付費的畫面。當使用 Metamask 時,這段流程不需要特別去做設定。

假設呼教 send 來呼叫 myContract 中的 myMethod 並且帶入參數 123
其呼叫方式就會如下:

myContract.methods.myMethod(123).send();

取得 Zombie 資料

還記得在 Contract 中

透過 Zombie 陣列儲存所有產生出來的 zombie 物件

Zombie[] public zombies;

因為宣告為 public , solidity 會自動產生 getter

透過 web3.js 要取得其中某個 id 的功能會實作如下:

function getZombieDetails(id) {
  return cryptoZombies.methods.zombies(id).call()
}

// Call the function and do something with the result:
getZombieDetails(15)
.then(function(result) {
  console.log("Zombie 15: " + JSON.stringify(result));
});

特別注意到 call 與 send 回傳的都是 Promise

而會傳回來的結果會是一個 javacript 物件

如下:

{
  "name": "H4XF13LD MORRIS'S COOLER OLDER BROTHER",
  "dna": "1337133713371337",
  "level": "9999",
  "readyTime": "1522498671",
  "winCount": "999999999",
  "lossCount": "0" // Obviously.
}

實作 zombieToOwner

  1. 宣告 function zombieToOwner
    需要一個參數 id
    回傳值是 cryptoZombies.methods.zombieToOwner(id).call()
  2. 宣告 function getZombiesByOwner
    需要一個參數 address
    回傳值是 cryptoZombies.methods.getZombiesByOwner(address).call()
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <title>CryptoZombies front-end</title>
    <script language="javascript" type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
    <script language="javascript" type="text/javascript" src="web3.min.js"></script>
    <script language="javascript" type="text/javascript" src="cryptozombies_abi.js"></script>
  </head>
  <body>

    <script>
      var cryptoZombies;

      function startApp() {
        var cryptoZombiesAddress = "YOUR_CONTRACT_ADDRESS";
        cryptoZombies = new web3js.eth.Contract(cryptoZombiesABI, cryptoZombiesAddress);
      }

      function getZombieDetails(id) {
        return cryptoZombies.methods.zombies(id).call()
      }

      // 1. Define `zombieToOwner` here
      function zombieToOwner(id) {
        return cryptoZombies.methods.zombieToOwner(id).call()
      }
      // 2. Define `getZombiesByOwner` here
      function getZombiesByOwner(owner) {
        return cryptoZombies.methods.getZombiesByOwner(owner).call()
      }
      window.addEventListener('load', function() {

        // Checking if Web3 has been injected by the browser (Mist/MetaMask)
        if (typeof web3 !== 'undefined') {
          // Use Mist/MetaMask's provider
          web3js = new Web3(web3.currentProvider);
        } else {
          // Handle the case where the user doesn't have Metamask installed
          // Probably show them a message prompting them to install Metamask
        }

        // Now you can start your app & access web3 freely:
        startApp()

      })
    </script>
  </body>
</html>

Metamask 與 Accounts

在前端應用,首頁需要顯示使用者所擁有的所有 Zombies

所以需要使用前面的 getZombiesByOwner(onwer) 來完成

然而,要怎麼拿到使用者的 address 呢?

以下將來講述如何在 Metamask 拿取使用者 address

在 Metamask 讀取使用者帳號

Metamask 讓使用者可以使用 Chrome Extension 管理多個帳號

當把 Metamask 引入 Web3 後

可以使用以下語法來取得當下活躍的帳號

var userAccount = web3.eth.accounts[0];

因為在 Metamask 可以讓使用者透過介面切換不同帳號

所以必須要注意每次帳號切換時狀態的改變

作法如下:

var accountInterval = setInterval(function() {
  // Check if account has changed
  if (web3.eth.accounts[0] !== userAccount) {
    userAccount = web3.eth.accounts[0];
    // Call some function to update the UI with the new account
    updateInterface();
  }
}, 100);

當然如果透過 Rx 的作法可以比較優雅的去檢查帳號更改

這邊就不深入討論

實作顯示 Zombie 的邏輯

  1. 宣告 var userAccount;
  2. 在 startApp() 最後 ,呼叫 accountInterval
  3. 把 accountInterval 的 updateInterface() 改成 getZombiesByOwner(userAccount)
  4. 在 getZombiesByOwner 之後 使用 then 然後內部帶入 displayZombies
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <title>CryptoZombies front-end</title>
    <script language="javascript" type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
    <script language="javascript" type="text/javascript" src="web3.min.js"></script>
    <script language="javascript" type="text/javascript" src="cryptozombies_abi.js"></script>
  </head>
  <body>

    <script>
      var cryptoZombies;
      // 1. declare `userAccount` here
      var userAccount;
      function startApp() {
        var cryptoZombiesAddress = "YOUR_CONTRACT_ADDRESS";
        cryptoZombies = new web3js.eth.Contract(cryptoZombiesABI, cryptoZombiesAddress);

        // 2. Create `setInterval` code here
		var accountInterval = setInterval(function() {
		  // Check if account has changed
		  if (web3.eth.accounts[0] !== userAccount) {
			userAccount = web3.eth.accounts[0];
			// Call some function to update the UI with the new account
			getZombiesByOwner(userAccount).then(displayZombies);
		  }
		}, 100);
      }

      function getZombieDetails(id) {
        return cryptoZombies.methods.zombies(id).call()
      }

      function zombieToOwner(id) {
        return cryptoZombies.methods.zombieToOwner(id).call()
      }

      function getZombiesByOwner(owner) {
        return cryptoZombies.methods.getZombiesByOwner(owner).call()
      }

      window.addEventListener('load', function() {

        // Checking if Web3 has been injected by the browser (Mist/MetaMask)
        if (typeof web3 !== 'undefined') {
          // Use Mist/MetaMask's provider
          web3js = new Web3(web3.currentProvider);
        } else {
          // Handle the case where the user doesn't have Metamask installed
          // Probably show them a message prompting them to install Metamask
        }

        // Now you can start your app & access web3 freely:
        startApp()

      })
    </script>
  </body>
</html>

顯示 Zombies 邏輯

顯示 Zombie 資料

首先會建立一個 DOM

如下

<div id="zombies"></div>

然後透過 getZombiesByOwner

會得到所有 zombie 的 id 陣列

顯示的邏輯如下

  1. 清除原本的 #zombies 內容
  2. loop 所有的 id 逐步透過 getZombieDetails(id) 取得所有內容
  3. 把顯示出來的資料 append 到 #zombies上

邏輯如下

// Look up zombie details from our contract. Returns a `zombie` object
getZombieDetails(id)
.then(function(zombie) {
  // Using ES6's "template literals" to inject variables into the HTML.
  // Append each one to our #zombies div
  $("#zombies").append(`<div class="zombie">
    <ul>
      <li>Name: ${zombie.name}</li>
      <li>DNA: ${zombie.dna}</li>
      <li>Level: ${zombie.level}</li>
      <li>Wins: ${zombie.winCount}</li>
      <li>Losses: ${zombie.lossCount}</li>
      <li>Ready Time: ${zombie.readyTime}</li>
    </ul>
  </div>`);
});

透過 sprite 方式展現 zombies

上面的範例是透過文字展現

然而也可以透過以下邏輯來用圖片展示

// Get an integer 1-7 that represents our zombie head:
var head = parseInt(zombie.dna.substring(0, 2)) % 7 + 1

// We have 7 head images with sequential filenames:
var headSrc = "../assets/zombieparts/head-" + head + ".png"

可以使用 sprite 元件化的展示不同 zombie 的特性。

實作 displayZombies

  1. 加入 $("#zombies").empty();
  2. 加入 for (id of ids)
  3. 把上面 getZombieDetails(id) 放到 for body
    並且把 $("#zombies").append(...) 放入
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <title>CryptoZombies front-end</title>
    <script language="javascript" type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
    <script language="javascript" type="text/javascript" src="web3.min.js"></script>
    <script language="javascript" type="text/javascript" src="cryptozombies_abi.js"></script>
  </head>
  <body>
    <div id="zombies"></div>

    <script>
      var cryptoZombies;
      var userAccount;

      function startApp() {
        var cryptoZombiesAddress = "YOUR_CONTRACT_ADDRESS";
        cryptoZombies = new web3js.eth.Contract(cryptoZombiesABI, cryptoZombiesAddress);

        var accountInterval = setInterval(function() {
          // Check if account has changed
          if (web3.eth.accounts[0] !== userAccount) {
            userAccount = web3.eth.accounts[0];
            // Call a function to update the UI with the new account
            getZombiesByOwner(userAccount)
            .then(displayZombies);
          }
        }, 100);
      }

      function displayZombies(ids) {
        // Start here
	$("#zombies").empty();
	for (id of ids) {
	  getZombieDetails(id).then(function(zombie) {
		$("#zombies").append(`<div class="zombie">
		  <ul>
			<li>Name: ${zombie.name}</li>
			<li>DNA: ${zombie.dna}</li>
			<li>Level: ${zombie.level}</li>
			<li>Wins: ${zombie.winCount}</li>
			<li>Losses: ${zombie.lossCount}</li>
			<li>Ready Time: ${zombie.readyTime}</li>
		  </ul>
		</div>`);
	  });
	}  
      }

      function getZombieDetails(id) {
        return cryptoZombies.methods.zombies(id).call()
      }

      function zombieToOwner(id) {
        return cryptoZombies.methods.zombieToOwner(id).call()
      }

      function getZombiesByOwner(owner) {
        return cryptoZombies.methods.getZombiesByOwner(owner).call()
      }

      window.addEventListener('load', function() {

        // Checking if Web3 has been injected by the browser (Mist/MetaMask)
        if (typeof web3 !== 'undefined') {
          // Use Mist/MetaMask's provider
          web3js = new Web3(web3.currentProvider);
        } else {
          // Handle the case where the user doesn't have Metamask installed
          // Probably show them a message prompting them to install Metamask
        }

        // Now you can start your app & access web3 freely:
        startApp()

      })
    </script>
  </body>
</html>

實作 sending Transatcions

send 功能特性

  1. send 需要一個 sender address
  2. send 會花費 gas
  3. 因為 send 會寫入交易,因此會花一段時間才完成
    至少需要分鐘級的 deplay
    即使很快把 transaction 送上去也要花一段時間才能同步

建立 Zombies 功能

在 Contract 裡功能如下

function createRandomZombie(string _name) public {
  require(ownerZombieCount[msg.sender] == 0);
  uint randDna = _generateRandomDna(_name);
  randDna = randDna - randDna % 100;
  _createZombie(_name, randDna);
}

在前端呼叫的方式如下

function createRandomZombie(name) {
  // This is going to take a while, so update the UI to let the user know
  // the transaction has been sent
  $("#txStatus").text("Creating new zombie on the blockchain. This may take a while...");
  // Send the tx to our contract:
  return cryptoZombies.methods.createRandomZombie(name)
  .send({ from: userAccount })
  .on("receipt", function(receipt) {
    $("#txStatus").text("Successfully created " + name + "!");
    // Transaction was accepted into the blockchain, let's redraw the UI
    getZombiesByOwner(userAccount).then(displayZombies);
  })
  .on("error", function(error) {
    // Do something to alert the user their transaction has failed
    $("#txStatus").text(error);
  });
}

當傳送一個交易到 Web3 provider會有以下事件要接收:

  • receipt: 當交易被接收時,就會收到這個 event。代表執行成功,狀態更新到鏈上了
  • error: 當交易失敗時,就會收到這個 event。代表資料狀態沒有被更新到鏈上,可能是 gas 不足或是其他原因。通常會做一個顯示讓使用者決定是否要重送。

備註 可以對 send 的呼叫指定 gas 與 gasPrice,例如send({from: userAccount, gas: 3000000})。如果不指定,MetaMask 會讓使用者選擇這些值。

實作 createRandomZombie

  1. 建立 function createRandomZombie 如範例
  2. 建立 function feedOnKitty 內容類似於 createRandomZombie 但做以下修改
    2.1 修改成兩個參數 zombieId, kittyId
    2.2 修改 #txStatus 為 "Eating a kitty. This may take a while..."
    2.3 呼叫 contract 的 feedOnKitty 帶入 zombieId, kittyId
    2.4 成功時,修改 #txStatus 為 "Ate a kitty and spawned a new Zombie!"
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <title>CryptoZombies front-end</title>
    <script language="javascript" type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
    <script language="javascript" type="text/javascript" src="web3.min.js"></script>
    <script language="javascript" type="text/javascript" src="cryptozombies_abi.js"></script>
  </head>
  <body>
    <div id="txStatus"></div>
    <div id="zombies"></div>

    <script>
      var cryptoZombies;
      var userAccount;

      function startApp() {
        var cryptoZombiesAddress = "YOUR_CONTRACT_ADDRESS";
        cryptoZombies = new web3js.eth.Contract(cryptoZombiesABI, cryptoZombiesAddress);

        var accountInterval = setInterval(function() {
          // Check if account has changed
          if (web3.eth.accounts[0] !== userAccount) {
            userAccount = web3.eth.accounts[0];
            // Call a function to update the UI with the new account
            getZombiesByOwner(userAccount)
            .then(displayZombies);
          }
        }, 100);
      }

      function displayZombies(ids) {
        $("#zombies").empty();
        for (id of ids) {
          // Look up zombie details from our contract. Returns a `zombie` object
          getZombieDetails(id)
          .then(function(zombie) {
            // Using ES6's "template literals" to inject variables into the HTML.
            // Append each one to our #zombies div
            $("#zombies").append(`<div class="zombie">
              <ul>
                <li>Name: ${zombie.name}</li>
                <li>DNA: ${zombie.dna}</li>
                <li>Level: ${zombie.level}</li>
                <li>Wins: ${zombie.winCount}</li>
                <li>Losses: ${zombie.lossCount}</li>
                <li>Ready Time: ${zombie.readyTime}</li>
              </ul>
            </div>`);
          });
        }
      }

      // Start here
      function createRandomZombie(name) {
        // This is going to take a while, so update the UI to let the user know
        // the transaction has been sent
        $("#txStatus").text("Creating new zombie on the blockchain. This may take a while...");
        // Send the tx to our contract:
        return cryptoZombies.methods.createRandomZombie(name)
        .send({ from: userAccount })
        .on("receipt", function(receipt) {
          $("#txStatus").text("Successfully created " + name + "!");
          // Transaction was accepted into the blockchain, let's redraw the UI
          getZombiesByOwner(userAccount).then(displayZombies);
        })
        .on("error", function(error) {
          // Do something to alert the user their transaction has failed
          $("#txStatus").text(error);
        });
      }
      function feedOnKitty(zombieId, kittyId) {
        // This is going to take a while, so update the UI to let the user know
        // the transaction has been sent
        $("#txStatus").text("Eating a kitty. This may take a while...");
        // Send the tx to our contract:
        return cryptoZombies.methods.feedOnKitty(zombieId, kittyId)
        .send({ from: userAccount })
        .on("receipt", function(receipt) {
          $("#txStatus").text("Ate a kitty and spawned a new Zombie!");
          // Transaction was accepted into the blockchain, let's redraw the UI
          getZombiesByOwner(userAccount).then(displayZombies);
        })
        .on("error", function(error) {
          // Do something to alert the user their transaction has failed
          $("#txStatus").text(error);
        });

      }
      function getZombieDetails(id) {
        return cryptoZombies.methods.zombies(id).call()
      }

      function zombieToOwner(id) {
        return cryptoZombies.methods.zombieToOwner(id).call()
      }

      function getZombiesByOwner(owner) {
        return cryptoZombies.methods.getZombiesByOwner(owner).call()
      }

      window.addEventListener('load', function() {

        // Checking if Web3 has been injected by the browser (Mist/MetaMask)
        if (typeof web3 !== 'undefined') {
          // Use Mist/MetaMask's provider
          web3js = new Web3(web3.currentProvider);
        } else {
          // Handle the case where the user doesn't have Metamask installed
          // Probably show them a message prompting them to install Metamask
        }

        // Now you can start your app & access web3 freely:
        startApp()

      })
    </script>
  </body>
</html>

實作 levelup

接下來要實作 attack, changeName, changeDna 的前端互動

這3個 function 都與 level 相關

首先先來討論一種特別的 function

payable

在 ZombieHelper 內有一個 levelUp function 如下

function levelUp(uint _zombieId) external payable {
  require(msg.value == levelUpFee);
  zombies[_zombieId].level++;
}

這類 function 可以接收 sender 傳遞 ether 到 Contract

並且需要指定要傳輸多少單位的 wei

wei

wei 是 ether 的最小單位。

兌換量如下 : 10^18 wei = 1 ether

// This will convert 1 ETH to Wei
web3js.utils.toWei("1");

在 Contract 裡,已經設定了 levelUpFee = 0.001 ether

所以要每次呼叫 levelUp 就需要傳輸 0.001 Ether

如下:

cryptoZombies.methods.levelUp(zombieId)
.send({ from: userAccount, value: web3js.utils.toWei("0.001", "ether") })

實作 levelUp

  1. 建立一個 function levelUp 類似於 feedOnKitty
  2. 需要一個參數 zombieId
  3. 在送 transation 前,需要顯示 txStatus 為 "Leveling up your zombie..."
  4. 當呼叫 Contract 的 levelUp 時,需要消耗 "0.001" ETH coverted to Wei
  5. 當成功送出 transaction 後,需要顯示 "Power overwhelming! Zombie successfully leveled up"
  6. 不需要更新 UI,因為只有更改 level
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <title>CryptoZombies front-end</title>
    <script language="javascript" type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
    <script language="javascript" type="text/javascript" src="web3.min.js"></script>
    <script language="javascript" type="text/javascript" src="cryptozombies_abi.js"></script>
  </head>
  <body>
    <div id="txStatus"></div>
    <div id="zombies"></div>

    <script>
      var cryptoZombies;
      var userAccount;

      function startApp() {
        var cryptoZombiesAddress = "YOUR_CONTRACT_ADDRESS";
        cryptoZombies = new web3js.eth.Contract(cryptoZombiesABI, cryptoZombiesAddress);

        var accountInterval = setInterval(function() {
          // Check if account has changed
          if (web3.eth.accounts[0] !== userAccount) {
            userAccount = web3.eth.accounts[0];
            // Call a function to update the UI with the new account
            getZombiesByOwner(userAccount)
            .then(displayZombies);
          }
        }, 100);
      }

      function displayZombies(ids) {
        $("#zombies").empty();
        for (id of ids) {
          // Look up zombie details from our contract. Returns a `zombie` object
          getZombieDetails(id)
          .then(function(zombie) {
            // Using ES6's "template literals" to inject variables into the HTML.
            // Append each one to our #zombies div
            $("#zombies").append(`<div class="zombie">
              <ul>
                <li>Name: ${zombie.name}</li>
                <li>DNA: ${zombie.dna}</li>
                <li>Level: ${zombie.level}</li>
                <li>Wins: ${zombie.winCount}</li>
                <li>Losses: ${zombie.lossCount}</li>
                <li>Ready Time: ${zombie.readyTime}</li>
              </ul>
            </div>`);
          });
        }
      }

      function createRandomZombie(name) {
        // This is going to take a while, so update the UI to let the user know
        // the transaction has been sent
        $("#txStatus").text("Creating new zombie on the blockchain. This may take a while...");
        // Send the tx to our contract:
        return cryptoZombies.methods.createRandomZombie(name)
        .send({ from: userAccount })
        .on("receipt", function(receipt) {
          $("#txStatus").text("Successfully created " + name + "!");
          // Transaction was accepted into the blockchain, let's redraw the UI
          getZombiesByOwner(userAccount).then(displayZombies);
        })
        .on("error", function(error) {
          // Do something to alert the user their transaction has failed
          $("#txStatus").text(error);
        });
      }

      function feedOnKitty(zombieId, kittyId) {
        $("#txStatus").text("Eating a kitty. This may take a while...");
        return cryptoZombies.methods.feedOnKitty(zombieId, kittyId)
        .send({ from: userAccount })
        .on("receipt", function(receipt) {
          $("#txStatus").text("Ate a kitty and spawned a new Zombie!");
          getZombiesByOwner(userAccount).then(displayZombies);
        })
        .on("error", function(error) {
          $("#txStatus").text(error);
        });
      }

      // Start here
      function levelUp(zombieId) {
        $("#txStatus").text("Leveling up your zombie...");
        return cryptoZombies.methods.levelUp(zombieId)
        .send({ from: userAccount, value: web3js.utils.toWei("0.001", "ether") })
        .on("receipt", function(receipt) {
          $("#txStatus").text("Power overwhelming! Zombie successfully leveled up");
        })
        .on("error", function(error) {
          $("#txStatus").text(error);
        });
      }
      function getZombieDetails(id) {
        return cryptoZombies.methods.zombies(id).call()
      }

      function zombieToOwner(id) {
        return cryptoZombies.methods.zombieToOwner(id).call()
      }

      function getZombiesByOwner(owner) {
        return cryptoZombies.methods.getZombiesByOwner(owner).call()
      }

      window.addEventListener('load', function() {

        // Checking if Web3 has been injected by the browser (Mist/MetaMask)
        if (typeof web3 !== 'undefined') {
          // Use Mist/MetaMask's provider
          web3js = new Web3(web3.currentProvider);
        } else {
          // Handle the case where the user doesn't have Metamask installed
          // Probably show them a message prompting them to install Metamask
        }

        // Now you can start your app & access web3 freely:
        startApp()

      })
    </script>
  </body>
</html>

訂閱 Events

為了能有效的顯示變動後的結果

因此可以透過訂閱在 Contract 內部的 Event 來做有效率的更新。

監聽 Zombie 的產生

在 zombiefactory.sol 有一個 NewZombie 事件如下

event NewZombie(uint zombieId, string name, uint dna);

在 Web3.js 可以採用以下的方式來監聽這個事件

cryptoZombies.events.NewZombie()
.on("data", function(event) {
  let zombie = event.returnValues;
  // We can access this event's 3 return values on the `event.returnValues` object:
  console.log("A new zombie was born!", zombie.zombieId, zombie.name, zombie.dna);
}).on("error", console.error);

使用 indexed 來做過濾

為了要過濾與某些條件相關的 event ,

可以在 Contract 的 Event 宣告增加 indexed 關鍵字
如下:

event Transfer(address indexed _from, address indexed _to, uint256 _tokenId);

在這個例子,因為 _from, _to 都是宣告為 indexed

代表說可以再前端使用 filter 來針對這兩個參數來做過慮

如下

// Use `filter` to only fire this code when `_to` equals `userAccount`
cryptoZombies.events.Transfer({ filter: { _to: userAccount } })
.on("data", function(event) {
  let data = event.returnValues;
  // The current user just received a zombie!
  // Do something here to update the UI to show it
}).on("error", console.error);

查訊過去的 Event

透過 getPastEvents 這個 function

可以指定 fromBlock 與 toBlock 來查訊特定 block 發生的 Event

如下:

cryptoZombies.getPastEvents("NewZombie", { fromBlock: 0, toBlock: "latest" })
.then(function(events) {
  // `events` is an array of `event` objects that we can iterate, like we did above
  // This code will get us a list of every zombie that was ever created
});

因為可以從過去 event log 從最初的狀態做查詢

所以有一種特別的使用情境: 透過 events log 來取代 storage

因為 storage 是很昂貴的,查詢 event 花費不多

代價是 event 可讀性並不高,因此會增加 Contract 複雜度。

但如果是要查歷史性資料可以透過這種方式來節省 gas

實作監聽 Transfer 的邏輯

  1. 新增 cryptoZombies.events.Transfer 在 startApp
  2. 然後加入更新 UI getZombiesByOwner(userAccount).then(displayZombies);
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <title>CryptoZombies front-end</title>
    <script language="javascript" type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
    <script language="javascript" type="text/javascript" src="web3.min.js"></script>
    <script language="javascript" type="text/javascript" src="cryptozombies_abi.js"></script>
  </head>
  <body>
    <div id="txStatus"></div>
    <div id="zombies"></div>

    <script>
      var cryptoZombies;
      var userAccount;

      function startApp() {
        var cryptoZombiesAddress = "YOUR_CONTRACT_ADDRESS";
        cryptoZombies = new web3js.eth.Contract(cryptoZombiesABI, cryptoZombiesAddress);

        var accountInterval = setInterval(function() {
          // Check if account has changed
          if (web3.eth.accounts[0] !== userAccount) {
            userAccount = web3.eth.accounts[0];
            // Call a function to update the UI with the new account
            getZombiesByOwner(userAccount)
            .then(displayZombies);
          }
        }, 100);

        // Start here
	cryptoZombies.events.Transfer({ filter: { _to: userAccount } })
	.on("data", function(event) {
	  let data = event.returnValues;
	  // The current user just received a zombie!
	  // Do something here to update the UI to show it
	  getZombiesByOwner(userAccount).then(displayZombies);		
	}).on("error", console.error);
  
      }

      function displayZombies(ids) {
        $("#zombies").empty();
        for (id of ids) {
          // Look up zombie details from our contract. Returns a `zombie` object
          getZombieDetails(id)
          .then(function(zombie) {
            // Using ES6's "template literals" to inject variables into the HTML.
            // Append each one to our #zombies div
            $("#zombies").append(`<div class="zombie">
              <ul>
                <li>Name: ${zombie.name}</li>
                <li>DNA: ${zombie.dna}</li>
                <li>Level: ${zombie.level}</li>
                <li>Wins: ${zombie.winCount}</li>
                <li>Losses: ${zombie.lossCount}</li>
                <li>Ready Time: ${zombie.readyTime}</li>
              </ul>
            </div>`);
          });
        }
      }

      function createRandomZombie(name) {
        // This is going to take a while, so update the UI to let the user know
        // the transaction has been sent
        $("#txStatus").text("Creating new zombie on the blockchain. This may take a while...");
        // Send the tx to our contract:
        return cryptoZombies.methods.createRandomZombie(name)
        .send({ from: userAccount })
        .on("receipt", function(receipt) {
          $("#txStatus").text("Successfully created " + name + "!");
          // Transaction was accepted into the blockchain, let's redraw the UI
          getZombiesByOwner(userAccount).then(displayZombies);
        })
        .on("error", function(error) {
          // Do something to alert the user their transaction has failed
          $("#txStatus").text(error);
        });
      }

      function feedOnKitty(zombieId, kittyId) {
        $("#txStatus").text("Eating a kitty. This may take a while...");
        return cryptoZombies.methods.feedOnKitty(zombieId, kittyId)
        .send({ from: userAccount })
        .on("receipt", function(receipt) {
          $("#txStatus").text("Ate a kitty and spawned a new Zombie!");
          getZombiesByOwner(userAccount).then(displayZombies);
        })
        .on("error", function(error) {
          $("#txStatus").text(error);
        });
      }

      function levelUp(zombieId) {
        $("#txStatus").text("Leveling up your zombie...");
        return cryptoZombies.methods.levelUp(zombieId)
        .send({ from: userAccount, value: web3.utils.toWei("0.001", "ether") })
        .on("receipt", function(receipt) {
          $("#txStatus").text("Power overwhelming! Zombie successfully leveled up");
        })
        .on("error", function(error) {
          $("#txStatus").text(error);
        });
      }

      function getZombieDetails(id) {
        return cryptoZombies.methods.zombies(id).call()
      }

      function zombieToOwner(id) {
        return cryptoZombies.methods.zombieToOwner(id).call()
      }

      function getZombiesByOwner(owner) {
        return cryptoZombies.methods.getZombiesByOwner(owner).call()
      }

      window.addEventListener('load', function() {

        // Checking if Web3 has been injected by the browser (Mist/MetaMask)
        if (typeof web3 !== 'undefined') {
          // Use Mist/MetaMask's provider
          web3js = new Web3(web3.currentProvider);
        } else {
          // Handle the case where the user doesn't have Metamask installed
          // Probably show them a message prompting them to install Metamask
        }

        // Now you can start your app & access web3 freely:
        startApp()

      })
    </script>
  </body>
</html>

到此,就能再 Transfer 發生時,有效更新 Zombie 顯示了


上一篇
從以太坊白皮書理解 web 3 概念 - Day13
下一篇
從以太坊白皮書理解 web 3 概念 - Day15
系列文
從以太坊白皮書理解 web 3 概念32
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言