# 區塊鏈web3.js實戰教學文件 [TOC] ###### 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 * 直接嵌入程式碼 ```<!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> <!-- 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 * 內部: * [自己set up your own private ethereum node](https://hackmd.io/NO1UAwKsT3-0-yP6645KTQ) * host your own Ethereum node as a provider. * 外部: * 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 ``` javacript= 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補充 ``` javascript= <!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== ```== javascript 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 ![](https://i.imgur.com/GznukWJ.png) * ABI (Application Binary Interface)--->binary code之間的互動介面 * 和API類似(source code之間的互動) ``` javascript= <!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智能合約 ``` javascript = myContract.methods.myMethod(123).call() ``` * send * view or pure以外的function * send a transaction 會需要付gas,且metamask要求使用者付費 * 若以Metamask來當web3 provider--->會自動化處理上述兩步驟 * 程式碼解析 1---> ```javascript= <!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---> ```javascript= 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 ---> ```javascript= 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 ---> ```javascript= <!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 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);//把結果存在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 ---> ```javascript= <!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上 ```javascript= // This will convert 1 ETH to Wei web3js.utils.toWei("1"); ``` ```javascript= <!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只會) ```solidity= event Transfer(address indexed _from, address indexed _to, uint256 _tokenId); ``` * 在前端介面可以filter_from _to(因為兩者都是indexed) ```javascript= // 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 ---> ```solidity= event NewZombie(uint zombieId, string name, uint dna); ``` ```javascript= 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 ---> ```javascript= <!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> ``` * [web3.js events logs](https://ksin751119.medium.com/ethereum-dapp%E5%88%9D%E5%BF%83%E8%80%85%E4%B9%8B%E8%B7%AF-6-web3-javascript-events-logs-api-a53c1bf8f60e) * [如何使用Web3.js構建Dapp](https://www.chaindaily.cc/posts/07cdc471471df27012d46700ace22b23)