继续前面的学习。感兴趣的同学,可以参考HiBlock社区有个Solidity 教程进行学习。
第五章 ERC721 标准和加密收藏品
一个 代币 在以太坊基本上就是一个遵循一些共同规则的智能合约——即它实现了所有其他代币合约共享的一组标准函数,例如 transfer(address _to, uint256 _value) 和 balanceOf(address _owner).
由于所有 ERC20 代币共享具有相同名称的同一组函数,它们都可以以相同的方式进行交互。
这意味着如果你构建的应用程序能够与一个 ERC20 代币进行交互,那么它就也能够与任何 ERC20 代币进行交互。 这样一来,将来你就可以轻松地将更多的代币添加到你的应用中,而无需进行自定义编码。 你可以简单地插入新的代币合约地址,然后哗啦,你的应用程序有另一个它可以使用的代币了。
ERC721 代币是不能互换的,因为每个代币都被认为是唯一且不可分割的。 你只能以整个单位交易它们,并且每个单位都有唯一的 ID。
使用像 ERC721 这样的标准的优势就是,我们不必在我们的合约中实现拍卖或托管逻辑,这决定了玩家能够如何交易/出售我们的僵尸。 如果我们符合规范,其他人可以为加密可交易的 ERC721 资产搭建一个交易所平台,我们的 ERC721 僵尸将可以在该平台上使用。 所以使用代币标准相较于使用你自己的交易逻辑有明显的好处。
ERC721 标准:
contract ERC721 {
event Transfer(address indexed _from, address indexed _to, uint256 _tokenId);
event Approval(address indexed _owner, address indexed _approved, uint256 _tokenId);
function balanceOf(address _owner) public view returns (uint256 _balance);
function ownerOf(uint256 _tokenId) public view returns (address _owner);
function transfer(address _to, uint256 _tokenId) public;
function approve(address _to, uint256 _tokenId) public;
function takeOwnership(uint256 _tokenId) public;
}
- balanceOf: 这个函数只需要一个传入 address 参数,然后返回这个 address 拥有多少代币。
- ownerOf: 这个函数需要传入一个代币 ID 作为参数 (我们的情况就是一个僵尸 ID),然后返回该代币拥有者的 address。
ERC721 规范有两种不同的方法来转移代币:
(1) 第一种方法是代币的拥有者调用transfer 方法,传入他想转移到的 address 和他想转移的代币的 _tokenId。
(2) 第二种方法是代币拥有者首先调用 approve,然后传入与以上相同的参数。接着,该合约会存储谁被允许提取代币,通常存储到一个 mapping (uint256 => address) 里。然后,当有人调用 takeOwnership 时,合约会检查 msg.sender 是否得到拥有者的批准来提取代币,如果是,则将代币转移给他。
继承自多个合约:
contract SatoshiNakamoto is NickSzabo, HalFinney {
// 啧啧啧,宇宙的奥秘泄露了
}
合约安全增强: 溢出和下溢
为了防止这些情况,OpenZeppelin 建立了一个叫做 SafeMath 的 库(library),默认情况下可以防止这些问题。
一个库 是 Solidity 中一种特殊的合约。其中一个有用的功能是给原始数据类型增加一些方法。
比如,使用 SafeMath 库的时候,我们将使用 using SafeMath for uint256 这样的语法。 SafeMath 库有四个方法 — add, sub, mul, 以及 div。现在我们可以这样来让 uint256 调用这些方法:
using SafeMath for uint256;
uint256 a = 5;
uint256 b = a.add(3); // 5 + 3 = 8
uint256 c = a.mul(2); // 5 * 2 = 10
SafeMath 的部分代码如下:
library SafeMath {
function mul(uint256 a, uint256 b) internal pure returns (uint256) {
if (a == 0) {
return 0;
}
uint256 c = a * b;
assert(c / a == b);
return c;
}
function div(uint256 a, uint256 b) internal pure returns (uint256) {
// assert(b > 0); // Solidity automatically throws when dividing by 0
uint256 c = a / b;
// assert(a == b * c + a % b); // There is no case in which this doesn't hold
return c;
}
function sub(uint256 a, uint256 b) internal pure returns (uint256) {
assert(b <= a);
return a - b;
}
function add(uint256 a, uint256 b) internal pure returns (uint256) {
uint256 c = a + b;
assert(c >= a);
return c;
}
}
using SafeMath for uint;
// 这下我们可以为任何 uint 调用这些方法了
uint test = 2;
test = test.mul(3); // test 等于 6 了
test = test.add(5); // test 等于 11 了
注意 mul 和 add 其实都需要两个参数。 在我们声明了 using SafeMath for uint 后,我们用来调用这些方法的 uint 就自动被作为第一个参数传递进去了(在此例中就是 test)
assert 和 require 相似,若结果为否它就会抛出错误。 assert 和 require 区别在于,require 若失败则会返还给用户剩下的 gas, assert 则不会。所以大部分情况下,你写代码的时候会比较喜欢 require,assert 只在代码可能出现严重错误的时候使用,比如 uint 溢出。
第六章 应用前端和 Web3.js
当你想要调用一份智能合约的一个方法,你需要从其中一个节点中查找并告诉它:
- 智能合约的地址
- 你想调用的方法,以及
- 你想传入那个方法的参数
以太坊节点只能识别一种叫做 JSON-RPC 的语言。这种语言直接读起来并不好懂。幸运的是 Web3.js 把这些令人讨厌的查询语句都隐藏起来了, 所以你只需要与方便易懂的 JavaScript 界面进行交互即可。
CryptoZombies.methods.createRandomZombie("Vitalik Nakamoto 🤔")
.send({ from: "0xb60e8dd61c5d32be8058bb8eb970870f07233155", gas: "3000000" })
Web3 提供者
以太坊是由共享同一份数据的相同拷贝的 节点 构成的。 在 Web3.js 里设置 Web3 的 Provider(提供者) 告诉我们的代码应该和 哪个节点 交互来处理我们的读写。这就好像在传统的 Web 应用程序中为你的 API 调用设置远程 Web 服务器的网址。
Infura 是一个服务,它维护了很多以太坊节点并提供了一个缓存层来实现高速读取。你可以用他们的 API 来免费访问这个服务。 用 Infura 作为节点提供者,你可以不用自己运营节点就能很可靠地向以太坊发送、接收信息。
var web3 = new Web3(new Web3.providers.WebsocketProvider("wss://mainnet.infura.io/ws"));
这里是一些 Metamask 提供的示例代码,用来检查用户是否安装了MetaMask,如果没有安装就告诉用户需要安装MetaMask来使用我们的应用。
window.addEventListener('load', function() {
// 检查web3是否已经注入到(Mist/MetaMask)
if (typeof web3 !== 'undefined') {
// 使用 Mist/MetaMask 的提供者
web3js = new Web3(web3.currentProvider);
} else {
// 处理用户没安装的情况, 比如显示一个消息
// 告诉他们要安装 MetaMask 来使用我们的应用
}
// 现在你可以启动你的应用并自由访问 Web3.js:
startApp()
})
合约 ABI
ABI 意为应用二进制接口(Application Binary Interface)。 基本上,它是以 JSON 格式表示合约的方法,告诉 Web3.js 如何以合同理解的方式格式化函数调用。
实例化 Web3.js:
// 实例化 myContract
var myContract = new web3js.eth.Contract(myABI, myContractAddress);
调用和合约函数
Web3.js 有两个方法来调用我们合约的函数: call and send.
call 用来调用 view 和 pure 函数。它只运行在本地节点,不会在区块链上创建事务。
myContract.methods.myMethod(123).call()
send 将创建一个事务并改变区块链上的数据。你需要用 send 来调用任何非 view 或者 pure 的函数。
myContract.methods.myMethod(123).send()
相对 call 函数,send 函数有如下主要区别:
- send 一个事务需要一个 from 地址来表明谁在调用这个函数(也就是你 Solidity 代码里的 msg.sender )。 我们需要这是我们 DApp 的用户,这样一来 MetaMask 才会弹出提示让他们对事务签名。
- send 一个事务将花费 gas
- 在用户 send 一个事务到该事务对区块链产生实际影响之间有一个不可忽略的延迟。这是因为我们必须等待事务被包含进一个区块里,以太坊上一个区块的时间平均下来是15秒左右。如果当前在以太坊上有大量挂起事务或者用户发送了过低的 gas 价格,我们的事务可能需要等待数个区块才能被包含进去,往往可能花费数分钟。
获得 MetaMask中的用户账户
MetaMask 允许用户在扩展中管理多个账户。
我们可以通过这样来获取 web3 变量中激活的当前账户:
var userAccount = web3.eth.accounts[0]
检查账号是否切换:
var accountInterval = setInterval(function() {
// 检查账户是否切换
if (web3.eth.accounts[0] !== userAccount) {
userAccount = web3.eth.accounts[0];
// 调用一些方法来更新界面
updateInterface();
}
}, 100);
在实际应用中,你肯定想要在应用中使用诸如 React 或 Vue.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);
});
}
receipt 将在合约被包含进以太坊区块上以后被触发,这意味着僵尸被创建并保存进我们的合约了。
error 将在事务未被成功包含进区块后触发,比如用户未支付足够的 gas。我们需要在界面中通知用户事务失败以便他们可以再次尝试。
调用 Payable 函数
一个 wei 是以太的最小单位 — 1 ether 等于 10^18 wei
// 把 1 ETH 转换成 Wei
web3js.utils.toWei("1", "ether");
cryptoZombies.methods.levelUp(zombieId)
.send({ from: userAccount, value: web3js.utils.toWei("0.001","ether") })
订阅事件
solidity中的事件:
event NewZombie(uint zombieId, string name, uint dna);
js中的调用
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);
cryptoZombies.events.Transfer({ filter: { _to: userAccount } })
.on("data", function(event) {
let data = event.returnValues;
// 当前用户更新了一个僵尸!更新界面来显示
}).on('error', console.error);
因为你可以用这个方法来查询从最开始起的事件日志,这就有了一个非常有趣的用例: 用事件来作为一种更便宜的存储。若你还能记得,在区块链上保存数据是 Solidity 中最贵的操作之一。但是用事件就便宜太多太多了。
这里的短板是,事件不能从智能合约本身读取。但是,如果你有一些数据需要永久性地记录在区块链中以便可以在应用的前端中读取,这将是一个很好的用例。这些数据不会影响智能合约向前的状态。
总结
这里介绍了代币的代码以及相关的实现。以及web3的使用。都是一些很实用的内容。