接上篇 Web3.js,這節課繼續學習Web3.js 的相關知識。
一、傳送事務
這下我們的介面能檢測使用者的 MetaMask 賬戶,並自動在首頁顯示它們的殭屍大軍了,有沒有很棒?
現在我們來看看用 send
函式來修改我們智慧合約裡面的資料。
相對 call
函式,send
函式有如下主要區別:
- 1、
send
一個事務需要一個from
地址來表明誰在呼叫這個函式(也就是你 Solidity 程式碼裡的msg.sender
)。 我們需要這是我們 DApp 的使用者,這樣一來 MetaMask 才會彈出提示讓他們對事務簽名。 - 2、send 一個事務將花費 gas
- 3、在使用者
send
一個事務到該事務對區塊鏈產生實際影響之間有一個不可忽略的延遲。這是因為我們必須等待事務被包含進一個區塊裡,以太坊上一個區塊的時間平均下來是15秒左右。如果當前在以太坊上有大量掛起事務或者使用者傳送了過低的gas
價格,我們的事務可能需要等待數個區塊才能被包含進去,往往可能花費數分鐘。
所以在我們的程式碼中我們需要編寫邏輯來處理這部分非同步特性。
生成一個殭屍
我們來看一個合約中一個新使用者將要呼叫的第一個函式: createRandomZombie
.
作為複習,這裡是合約中的 Solidity
程式碼:
function createRandomZombie(string _name) public {
require(ownerZombieCount[msg.sender] == 0);
uint randDna = _generateRandomDna(_name);
randDna = randDna - randDna % 100;
_createZombie(_name, randDna);
}
這是如何在用 MetaMask 在 Web3.js 中呼叫這個函式的示例:
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
提供者,然後鏈式新增一些事件監聽:
-
receipt
將在合約被包含進以太坊區塊上以後被觸發,這意味著殭屍被建立並儲存進我們的合約了。 -
error
將在事務未被成功包含進區塊後觸發,比如使用者未支付足夠的 gas。我們需要在介面中通知使用者事務失敗以便他們可以再次嘗試。
注意:你可以在呼叫
send
時選擇指定gas
和gasPrice
, 例如:.send({ from: userAccount, gas: 3000000 })
。如果你不指定,MetaMask
將讓使用者自己選擇數值。
實戰演練
我們新增了一個div, 指定 ID 為 txStatus
— 這樣我們可以通過更新這個 div
來通知使用者事務的狀態。
- 1、在
displayZombies
下面, 複製貼上上面createRandomZombie
的程式碼。 - 2、我們來實現另外一個函式 feedOnKitty:
- 呼叫
feedOnKitty
的邏輯幾乎一樣 — 我們將傳送一個事務來呼叫這個函式,並且成功的事務會為我們建立一個殭屍,所以我們希望在成功後重新繪製介面。 - 在
createRandomZombie
下面複製貼上它的程式碼,改動這些地方: - a) 給其命名為 feedOnKitty, 它將接收兩個引數 zombieId 和 kittyId
- b) #txStatus 的文字內容將更新為: “正在吃貓咪,這將需要一會兒…”
- c) 讓其呼叫我們合約裡面的 feedOnKitty 函式並傳入相同的引數
- d) #txStatus 裡面的的成功資訊應該是 “吃了一隻貓咪並生成了一隻新殭屍!”
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>`);
});
}
}
// 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>
二、呼叫Payable函式
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?
一個 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
將非常相似。不過:
- 1、函式將接收一個引數,
zombieId
- 2、在傳送事務之前,
txStatus
的文字應該是 “正在升級您的殭屍…” - 3、當它呼叫合約裡的levelUp時,它應該傳送”0.001″ ETH,並用
toWei
轉換,如同上面例子裡那樣。 - 4、成功之後應該顯示 “不得了了!殭屍成功升級啦!”
- 5、我們 不 需要在呼叫
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);
注意這段程式碼將在 任何 殭屍生成的時候激發一個警告資訊——而不僅僅是當前用使用者的殭屍。如果我們只想對當前使用者發出提醒呢?
使用indexed
為了篩選僅和當前使用者相關的事件,我們的 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事件和MetaMask
上面的示例程式碼是針對 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
已經初始化了。
- 1、在
startApp()
底部,為cryptoZombies.events.Transfer
複製貼上上面的2行事件監聽程式碼塊 - 2、複製監聽
Transfer
事件的程式碼塊,並用_to: userAccount
過濾。要記得把cryptoZombies
換成 czEvents 好在這 裡使用 Infura 而不是MetaMask
來作為提供者。 - 3、用
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>