接上篇 Web3.js,這節課繼續學習Web3.js 的相關知識。
這下咱們的界面能檢測用戶的 MetaMask 帳戶,並自動在首頁顯示它們的殭屍大軍了,有沒有很棒?javascript
如今咱們來看看用 send
函數來修改咱們智能合約裏面的數據。html
相對 call
函數,send
函數有以下主要區別:前端
send
一個事務須要一個 from
地址來代表誰在調用這個函數(也就是你 Solidity 代碼裏的 msg.sender
)。 咱們須要這是咱們 DApp 的用戶,這樣一來 MetaMask 纔會彈出提示讓他們對事務簽名。send
一個事務到該事務對區塊鏈產生實際影響之間有一個不可忽略的延遲。這是由於咱們必須等待事務被包含進一個區塊裏,以太坊上一個區塊的時間平均下來是15秒左右。若是當前在以太坊上有大量掛起事務或者用戶發送了太低的 gas
價格,咱們的事務可能須要等待數個區塊才能被包含進去,每每可能花費數分鐘。因此在咱們的代碼中咱們須要編寫邏輯來處理這部分異步特性。java
咱們來看一個合約中一個新用戶將要調用的第一個函數: createRandomZombie
.jquery
做爲複習,這裏是合約中的 Solidity
代碼:web
function createRandomZombie(string _name) public { require(ownerZombieCount[msg.sender] == 0); uint randDna = _generateRandomDna(_name); randDna = randDna - randDna % 100; _createZombie(_name, randDna); }
這是如何在用 MetaMask 在 Web3.js 中調用這個函數的示例:ajax
function createRandomZombie(name) { // 這將須要一段時間,因此在界面中告訴用戶這一點 // 事務被髮送出去了 $("#txStatus").text("正在區塊鏈上建立殭屍,這將須要一下子..."); // 把事務發送到咱們的合約: return CryptoZombies.methods.createRandomZombie(name) .send({ from: userAccount }) .on("receipt", function(receipt) { $("#txStatus").text("成功生成了 " + name + "!"); // 事務被區塊連接受了,從新渲染界面 getZombiesByOwner(userAccount).then(displayZombies); }) .on("error", function(error) { // 告訴用戶合約失敗了 $("#txStatus").text(error); }); }
咱們的函數 send
一個事務到咱們的 Web3
提供者,而後鏈式添加一些事件監聽:segmentfault
receipt
將在合約被包含進以太坊區塊上之後被觸發,這意味着殭屍被建立並保存進咱們的合約了。error
將在事務未被成功包含進區塊後觸發,好比用戶未支付足夠的 gas。咱們須要在界面中通知用戶事務失敗以便他們能夠再次嘗試。注意:你能夠在調用send
時選擇指定gas
和gasPrice
, 例如:.send({ from: userAccount, gas: 3000000 })
。若是你不指定,MetaMask
將讓用戶本身選擇數值。
咱們添加了一個div, 指定 ID 爲 txStatus
— 這樣咱們能夠經過更新這個 div
來通知用戶事務的狀態。網絡
displayZombies
下面, 複製粘貼上面 createRandomZombie
的代碼。feedOnKitty
的邏輯幾乎同樣 — 咱們將發送一個事務來調用這個函數,而且成功的事務會爲咱們建立一個殭屍,因此咱們但願在成功後從新繪製界面。createRandomZombie
下面複製粘貼它的代碼,改動這些地方:index.html
app
<!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) { // 這將須要一段時間,因此在界面中告訴用戶這一點 // 事務被髮送出去了 $("#txStatus").text("正在區塊鏈上建立殭屍,這將須要一下子..."); // 把事務發送到咱們的合約: return CryptoZombies.methods.createRandomZombie(name) .send({ from: userAccount }) .on("receipt", function(receipt) { $("#txStatus").text("成功生成了 " + name + "!"); // 事務被區塊連接受了,從新渲染界面 getZombiesByOwner(userAccount).then(displayZombies); }) .on("error", function(error) { // 告訴用戶合約失敗了 $("#txStatus").text(error); }); } function feedOnKitty(zombieId, kittyId) { // 這將須要一段時間,因此在界面中告訴用戶這一點 // 事務被髮送出去了 $("#txStatus").text("正在吃貓咪,這將須要一下子..."); // 把事務發送到咱們的合約: return CryptoZombies.methods.feedOnKitty(zombieId, kittyId) .send({ from: userAccount }) .on("receipt", function(receipt) { $("#txStatus").text("吃了一隻貓咪並生成了一隻新殭屍!"); // 事務被區塊連接受了,從新渲染界面 getZombiesByOwner(userAccount).then(displayZombies); }) .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>
attack
, changeName
, 以及 changeDna
的邏輯將很是雷同,因此本課將不會花時間在上面。
實際上,在調用這些函數的時候已經有了很是多的重複邏輯。因此最好是重構代碼把相同的代碼寫成一個函數。(並對txStatus使用模板系統——咱們已經看到用相似
Vue.js
類的框架是多麼整潔)
咱們來看看另一種 Web3.js 中須要特殊對待的函數 — payable
函數。
回憶一下在 ZombieHelper
裏面,咱們添加了一個 payable
函數,用戶能夠用來升級:
function levelUp(uint _zombieId) external payable { require(msg.value == levelUpFee); zombies[_zombieId].level++; }
和函數一塊兒發送以太很是簡單,只有一點須要注意: 咱們須要指定發送多少 wei
,而不是以太。
一個 wei
是以太的最小單位 — 1 ether
等於 10^18 wei
太多0要數了,不過幸運的是 Web3.js 有一個轉換工具來幫咱們作這件事:
// 把 1 ETH 轉換成 Wei web3js.utils.toWei("1", "ether");
在咱們的 DApp 裏, 咱們設置了 levelUpFee = 0.001 ether
,因此調用 levelUp
方法的時候,咱們可讓用戶用如下的代碼同時發送 0.001
以太:
CryptoZombies.methods.levelUp(zombieId) .send({ from: userAccount, value: web3js.utils.toWei("0.001","ether") })
在 feedOnKitty
下面添加一個 levelUp
方法。代碼和 feedOnKitty
將很是類似。不過:
zombieId
txStatus
的文本應該是 "正在升級您的殭屍..."toWei
轉換,如同上面例子裏那樣。getZombiesByOwner
後從新繪製界面 — 由於在這裏咱們只是修改了殭屍的級別而已。index.html
<!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("正在升級您的殭屍..."); return CryptoZombies.methods.levelUp(zombieId) .send({ from: userAccount, value: web3js.utils.toWei("0.001", "ether") }) .on("receipt", function(receipt) { $("#txStatus").text("不得了了!殭屍成功升級啦!"); }) .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 和合約交互很是簡單直接——一旦你的環境創建起來, call
函數和 send
事務和普通的網絡API並無多少不一樣。
還有一點東西咱們想要講到——訂閱合約事件
若是你還記得 zombiefactory.sol
,每次新建一個殭屍後,咱們會觸發一個 NewZombie
事件:
event NewZombie(uint zombieId, string name, uint dna);
在 Web3.js裏, 你能夠 訂閱 一個事件,這樣你的 Web3 提供者能夠在每次事件發生後觸發你的一些代碼邏輯:
cryptoZombies.events.NewZombie() .on("data", function(event) { let zombie = event.returnValues; console.log("一個新殭屍誕生了!", zombie.zombieId, zombie.name, zombie.dna); }).on('error', console.error);
注意這段代碼將在 任何 殭屍生成的時候激發一個警告信息——而不只僅是當前用用戶的殭屍。若是咱們只想對當前用戶發出提醒呢?
爲了篩選僅和當前用戶相關的事件,咱們的 Solidity 合約將必須使用 indexed
關鍵字,就像咱們在 ERC721 實現中的Transfer 事件中那樣:
event Transfer(address indexed _from, address indexed _to, uint256 _tokenId);
在這種狀況下, 由於_from
和 _to
都是 indexed
,這就意味着咱們能夠在前端事件監聽中過濾事件.
cryptoZombies.events.Transfer({ filter: { _to: userAccount } }) .on("data", function(event) { let data = event.returnValues; // 當前用戶更新了一個殭屍!更新界面來顯示 }).on('error', console.error);
看到了吧, 使用 event
和 indexed
字段對於監聽合約中的更改並將其反映到 DApp 的前端界面中是很是有用的作法。
咱們甚至能夠用 getPastEvents
查詢過去的事件,並用過濾器 fromBlock
和 toBlock
給 Solidity 一個事件日誌的時間範圍("block" 在這裏表明以太坊區塊編號):
cryptoZombies.getPastEvents("NewZombie", { fromBlock: 0, toBlock: 'latest' }) .then(function(events) { // events 是能夠用來遍歷的 `event` 對象 // 這段代碼將返回給咱們從開始以來建立的殭屍列表 });
由於你能夠用這個方法來查詢從最開始起的事件日誌,這就有了一個很是有趣的用例: 用事件來做爲一種更便宜的存儲。
若你還能記得,在區塊鏈上保存數據是 Solidity 中最貴的操做之一。可是用事件就便宜太多太多了。
這裏的短板是,事件不能從智能合約自己讀取。可是,若是你有一些數據須要永久性地記錄在區塊鏈中以即可以在應用的前端中讀取,這將是一個很好的用例。這些數據不會影響智能合約向前的狀態。
舉個栗子,咱們能夠用事件來做爲殭屍戰鬥的歷史紀錄——咱們能夠在每次殭屍攻擊別人以及有一方勝出的時候產生一個事件。智能合約不須要這些數據來計算任何接下來的事情,可是這對咱們在前端向用戶展現來講是很是有用的東西。
上面的示例代碼是針對 Web3.js 最新版1.0的,此版本使用了 WebSockets 來訂閱事件。
可是,MetaMask 尚且不支持最新的事件 API (儘管如此,他們已經在實現這部分功能了, 點擊這裏 查看進度)
因此如今咱們必須使用一個單獨 Web3 提供者,它針對事件提供了WebSockets支持。 咱們能夠用 Infura
來像實例化第二份拷貝:
var web3Infura = new Web3(new Web3.providers.WebsocketProvider("wss://mainnet.infura.io/ws")); var czEvents = new web3Infura.eth.Contract(cryptoZombiesABI, cryptoZombiesAddress);
而後咱們將使用 czEvents.events.Transfer
來監聽事件,而再也不使用 cryptoZombies.events.Transfer
。咱們將繼續在課程的其餘部分使用 cryptoZombies.methods
。
未來,在 MetaMask 升級了 API 支持 Web3.js 後,咱們就不用這麼作了。可是如今咱們仍是要這麼作,以使用 Web3.js 更好的最新語法來監聽事件。
來添加一些代碼監聽 Transfer
事件,並在當前用戶得到一個新殭屍的時候爲他更新界面。
咱們將須要在 startApp
底部添加代碼,以保證在添加事件監聽器以前 cryptoZombies
已經初始化了。
startApp()
底部,爲 cryptoZombies.events.Transfer
複製粘貼上面的2行事件監聽代碼塊Transfer
事件的代碼塊,並用 _to: userAccount
過濾。要記得把 cryptoZombies
換成 czEvents 好在這 裏使用 Infura 而不是 MetaMask
來做爲提供者。getZombiesByOwner(userAccount).then(displayZombies);
來更新界面index.html
<!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 var web3Infura = new Web3(new Web3.providers.WebsocketProvider("wss: var czEvents = new web3Infura.eth.Contract(cryptoZombiesABI, cryptoZombiesAddress); czEvents.events.Transfer({ filter: { _to: userAccount } }) .on("data", function(event) { let data = event.returnValues; 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>