以太坊开发实战学习-Web3.js(十)
接上篇 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>