接上篇 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>