前面都在講關於 Contract 的部份
今天將透過 Lession 6 - App Front-ends & Web3.js 來學習網頁前端可以透過 Web3.js 與 Contract 互動的邏輯
Ethereum 網路是透過 Ethereum 節點所建立起來的。
當想要使用 Smart Contract 上的功能時
必須要與 Ethereum 節點溝通,並且傳遞以下資訊給節點:
而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>
<!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 來與 Ethereum 的節點溝通
Web3 Provider 就是一個元件用來建立與 Ethereum 的節點連線
在建立 Web3 Provider 元件時,需要把 Ethereum 的節點 URI當作參數帶入
Ethereum 的節點可以自己建立或是使用節點服務像 Infura
Infura 是一個提供 Ethereum 節點的服務,並且具有快取機制可以讓大量讀取變快,此外透過 Infura api 來讀取是免費的。
使用範例如下
var web3 = new Web3(new Web3.providers.WebsocketProvider("wss://mainnet.infura.io/ws"));
在區塊鏈中,使用者需要自行保存私鑰與處理交易簽章
而保存私鑰與處理交易簽章的系統就是所謂 Wallet
Metamask 是一個使用 chrome extension 技術製作的 Wallet
Metamask 可以用來產生簽章要做的私鑰並且保存,還有處理交易簽章
並且 Metamask 預設使用 Infura 作為 Web3 Provider
因此,如果要開發前端 Dapp 是一個方便的工具
然而,私鑰在區塊鏈是一個很重要代表身分的文件
所以放在 Metamask 這類軟體錢包有其風險性
因此在使用時,也要特別小心
過去也有假冒 Metamask 錢包的盜取密鑰攻擊。
因為 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();
});
<!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 發佈於鏈上之後,會產生一個 Contract Address,用來讓使用 Contract 透過位置與其互動。
ABI 代表 Application Binary Interface。是一種把 Contract 溝通介面以二進位的表達式。
當把 Contract 發佈到鏈上, Contract 會自動產生出 ABI。
Web3.js 會透過這個表達式與 Contract 做溝通。
// Instantiate myContract
var myContract = new web3js.eth.Contract(myABI, myContractAddress);
<!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>
Web3.js 提供了兩個 method 可以來呼教 Contract 的功能
call 是用來呼叫 view 與 pure function 。這類呼叫只是讀取值,不會影響到鏈上的資料。
假設要呼叫 myContract 中的 myMethod 並且帶入參數 123
其呼叫方式就會如下:
myContract.methods.myMethod(123).call()
send 是用來產生交易資料並且會改變鏈上資料狀態。所以當只是 view 或是 pure 就不需要使用 send。
備註 當使用 send 來發送交易將需會使用者付 gas 來處理交易,而錢包將會跳出付費的畫面。當使用 Metamask 時,這段流程不需要特別去做設定。
假設呼教 send 來呼叫 myContract 中的 myMethod 並且帶入參數 123
其呼叫方式就會如下:
myContract.methods.myMethod(123).send();
還記得在 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.
}
<!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>
在前端應用,首頁需要顯示使用者所擁有的所有 Zombies
所以需要使用前面的 getZombiesByOwner(onwer) 來完成
然而,要怎麼拿到使用者的 address 呢?
以下將來講述如何在 Metamask 拿取使用者 address
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 的作法可以比較優雅的去檢查帳號更改
這邊就不深入討論
<!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>
首先會建立一個 DOM
如下
<div id="zombies"></div>
然後透過 getZombiesByOwner
會得到所有 zombie 的 id 陣列
顯示的邏輯如下
邏輯如下
// 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>`);
});
上面的範例是透過文字展現
然而也可以透過以下邏輯來用圖片展示
// 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 的特性。
<!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>
在 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會有以下事件要接收:
備註 可以對 send 的呼叫指定 gas 與 gasPrice,例如send({from: userAccount, gas: 3000000})。如果不指定,MetaMask 會讓使用者選擇這些值。
<!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>
接下來要實作 attack, changeName, changeDna 的前端互動
這3個 function 都與 level 相關
首先先來討論一種特別的 function
在 ZombieHelper 內有一個 levelUp function 如下
function levelUp(uint _zombieId) external payable {
require(msg.value == levelUpFee);
zombies[_zombieId].level++;
}
這類 function 可以接收 sender 傳遞 ether 到 Contract
並且需要指定要傳輸多少單位的 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") })
<!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>
為了能有效的顯示變動後的結果
因此可以透過訂閱在 Contract 內部的 Event 來做有效率的更新。
在 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);
為了要過濾與某些條件相關的 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);
透過 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
<!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 顯示了