Try   HackMD

區塊鏈web3.js實戰教學文件

tags: Dapp 區塊鏈

定義web3.js

web3.js is a collection of libraries that allow you to interact with a local or remote ethereum node using HTTP, IPC or WebSocket.


安裝web3.js

  • 直接嵌入程式碼
<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>
    <!-- Include web3.js here -->
    <script language="javascript" type="text/javascript" src="web3.min.js"></script>
  </head>
  <body>

  </body>
</html>

  • 其他指令

    • Using NPM
      npm install web3

    • Using Yarn
      yarn add web3

    • Using Bower
      bower install web3


如何讓 nodes 與 blockchain 溝通(read/write in blockchain)

Ethereum is made up of nodes that all share a copy of the same data
  • 內部:
  • 外部:
    • Web3 Providers API->infura
      • tells our code which node we should be talking to handle our reads and writes
      • like setting the URL of the remote web server for your API calls in a traditional web app
window.addEventListener('load', function() {//addEventListner是網頁載入load的事件處理器 // 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() })

Event handler補充

<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <title>事件物件 Event object</title> <script type="text/javascript"> function init(){ var btn=document.getElementById("btn"); var handler=(e)=>{//e為事件物件 alert(e.clientX+",",e.clientY);//建立事件處理函式 }; btn.addEventListener("click",handler);//在btm物件上註冊事件處理器addEventListner //事件的名稱:"click" //處理器對應的處理函式:"handler" /* 1.使用者點擊按鈕,觸發click事件 2.瀏覽器主動收集和事件有關資訊,並製造出Event Object事件物件 var eventObj=事件物件; 3.呼叫已經註冊的事件處理器(事件處理函式)handler() handler(eventObj); */ document.addEventListener("keydown",(e)=>{ alert(e.keyCode);//keyCode為鼠標箭頭方向 }); } </script> </head> <body onload="init();"> <button id="btn">Click</button> </body> </html>

ABI

  • Web3.js 和 智能合約互動 需 (1)address (2)ABI
var myContract = new web3js.eth.Contract(myABI, myContractAddress); // Instantiate myContract
  • Dapp後端

    • 撰寫智能合約原始碼(solidity)/框架(truffle)上
    • 智能合約要先compile成bytecode(binary code)->EVM才可執行(based on EVM)
    • deploy合約->把bytecode(binary code)透過RPC 或 IPC儲存在鏈上(透過一個transaction)->最後取得獨一無二的transaction address
    • 若要寫程式呼叫這個智能合約,要把資料發送到transaction address
    • Ethereum node會根據輸入資料,決定要執行智能合約中哪一個function和輸入參數
  • Dapp前端

    • web3.js + RPC 接上 ethereum
      Image Not Showing Possible Reasons
      • The image file may be corrupted
      • The server hosting the image is unavailable
      • The image path is incorrect
      • The image format is not supported
      Learn More →
  • ABI (Application Binary Interface)->binary code之間的互動介面

    • 和API類似(source code之間的互動)
<!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> <!-- 1. Include cryptozombies_abi.js here --> <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); } 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>

Calling Contract Functions

  • Web3.js Calling Contract Functions方法有下列兩種

    • call
      • view pure修飾字會使用(read-only function 不會改變blockchain上的state 不會消耗 gas 使用者不需要透過metamask簽transaction)
      • 只會在local node run起來
      • 不會產生transaction
      • 下面程式碼使用myMethod()來call智能合約
      ​​​​     myContract.methods.myMethod(123).call()
      
    • send
      • view or pure以外的function
      • send a transaction 會需要付gas,且metamask要求使用者付費
      • 若以Metamask來當web3 provider->會自動化處理上述兩步驟
  • 程式碼解析 1->

<!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); //Instantiate cryptoZombies } 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>

  • 程式碼解析 2->
function getZombieDetails(id) { return cryptoZombies.methods.zombies(id).call() } // Call the function and do something with the result: getZombieDetails(15) .then(function(result) { //非同步async(promise) //等promise結束後,從 web3 provider得到回覆 //才會跑then繼續程式碼 //結果會存到function(result) console.log("Zombie 15: " + JSON.stringify(result)); });

Async(非同步處理方式)

  • 目的:為了解決連線MetaMask的user account若改變,UI要能顯現出來>

    • 用setInterval()來每隔100ms檢查
    • 連到錢包的帳號(web3.eth.accounts[0])
    • 使用者帳號(userAccount)
      • var userAccount = web3.eth.accounts[0]
      • let n = web3.eth.getBlockNumber();->紀錄 migrate transaction 在哪一個 block 上
  • 程式碼解析 1 ->

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);//每隔100ms會檢查一次 //var userAccount = web3.eth.accounts[0]
  • 程式碼解析 2 ->
<!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;var userAccount;function startApp() {var cryptoZombiesAddress = "YOUR_CONTRACT_ADDRESS"; ​ cryptoZombies = new web3js.eth.Contract(cryptoZombiesABI, cryptoZombiesAddress);var accountInterval = setInterval(function() {//check if account has changedif (web3.eth.accounts[0] !== userAccount) { ​ userAccount = web3.eth.accounts[0];// Call some function to update the UI with the new accountgetZombiesByOwner(userAccount).then(displayZombies);//把結果存在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>

如何從後端smart contract回傳資料到前端介面(以jQuery為例)

  • 程式碼解析 1 ->
<!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) { $("#zombies").empty(); //First clear the contents of the #zombies div, // if there's anything already inside it. // This way if the user changes their active MetaMask account, // it will clear their old zombie army before loading the new one. for(id of ids){ //Loop through each id, //and for each one call getZombieDetails(id) //to look up all the information for that zombie from our smart contract, 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>`); });//Put the information about that zombie into an HTML template //to format it for display, //and append that template to the #zombies 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 Transactions

  • 處理非同步async產生原因>(1)若很多transaction在pending on blockchain(2)有 user send gas price太低 (3)要等待大約15sec transaction被寫在block上
// This will convert 1 ETH to Wei web3js.utils.toWei("1");
<!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); }); } function levelUp(zombieId) { $("#txStatus").text("Leveling up your zombie..."); return cryptoZombies.methods.levelUp(zombieId) .send({ from: userAccount, value: web3js.utils.toWei("0.001", "ether") })//When it calls levelUp on the contract, it should send "0.001" ETH converted toWei, .on("receipt", function(receipt) { $("#txStatus").text("Power overwhelming! Zombie successfully leveled up");//success it should display the 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>

修飾字indexed

  • 定義:indexed 修飾 _from _to (indexed只會)
event Transfer(address indexed _from, address indexed _to, uint256 _tokenId);
  • 在前端介面可以filter_from _to(因為兩者都是indexed)
// Use `filter` to only fire this code when `_to` equals `userAccount` cryptoZombies.events.Transfer({ filter: { _to: userAccount } }) //filter for them in our event listener in our front end .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);

Querying past events

  • getPastEvents來 query past events
    • 用 fromBlock 和 toBlock 等 filters 給 solidity 1 個 time range for event logs(block )
      • events 比 storage 便宜
      • 但events在智能合約裡不可讀
  • calling functions sending transactions through web3.js
  • Subscribing to Events
  • 程式碼解析 1 ->
event NewZombie(uint zombieId, string name, uint dna);
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);

web3.js subscribe to an event ->web3 provider fire an event


  • 程式碼解析 2 ->
<!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); // 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! getZombiesByOwner(userAccount).then(displayZombies);// Do something here to update the UI to show it }).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>