繼續(xù)前面的學(xué)習(xí)融虽。感興趣的同學(xué)蕉世,可以參考HiBlock社區(qū)有個(gè)Solidity 教程進(jìn)行學(xué)習(xí)歉摧。
第五章 ERC721 標(biāo)準(zhǔn)和加密收藏品
一個(gè) 代幣 在以太坊基本上就是一個(gè)遵循一些共同規(guī)則的智能合約——即它實(shí)現(xiàn)了所有其他代幣合約共享的一組標(biāo)準(zhǔn)函數(shù)阶女,例如 transfer(address _to, uint256 _value) 和 balanceOf(address _owner).
由于所有 ERC20 代幣共享具有相同名稱的同一組函數(shù)堕义,它們都可以以相同的方式進(jìn)行交互它浅。
這意味著如果你構(gòu)建的應(yīng)用程序能夠與一個(gè) ERC20 代幣進(jìn)行交互羊娃,那么它就也能夠與任何 ERC20 代幣進(jìn)行交互翼馆。 這樣一來(lái)东且,將來(lái)你就可以輕松地將更多的代幣添加到你的應(yīng)用中启具,而無(wú)需進(jìn)行自定義編碼。 你可以簡(jiǎn)單地插入新的代幣合約地址珊泳,然后嘩啦鲁冯,你的應(yīng)用程序有另一個(gè)它可以使用的代幣了。
ERC721 代幣是不能互換的色查,因?yàn)槊總€(gè)代幣都被認(rèn)為是唯一且不可分割的薯演。 你只能以整個(gè)單位交易它們,并且每個(gè)單位都有唯一的 ID秧了。
使用像 ERC721 這樣的標(biāo)準(zhǔn)的優(yōu)勢(shì)就是跨扮,我們不必在我們的合約中實(shí)現(xiàn)拍賣或托管邏輯,這決定了玩家能夠如何交易/出售我們的僵尸验毡。 如果我們符合規(guī)范衡创,其他人可以為加密可交易的 ERC721 資產(chǎn)搭建一個(gè)交易所平臺(tái),我們的 ERC721 僵尸將可以在該平臺(tái)上使用晶通。 所以使用代幣標(biāo)準(zhǔn)相較于使用你自己的交易邏輯有明顯的好處璃氢。
ERC721 標(biāo)準(zhǔn):
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: 這個(gè)函數(shù)只需要一個(gè)傳入 address 參數(shù),然后返回這個(gè) address 擁有多少代幣狮辽。
- ownerOf: 這個(gè)函數(shù)需要傳入一個(gè)代幣 ID 作為參數(shù) (我們的情況就是一個(gè)僵尸 ID)一也,然后返回該代幣擁有者的 address。
ERC721 規(guī)范有兩種不同的方法來(lái)轉(zhuǎn)移代幣:
(1) 第一種方法是代幣的擁有者調(diào)用transfer 方法喉脖,傳入他想轉(zhuǎn)移到的 address 和他想轉(zhuǎn)移的代幣的 _tokenId椰苟。
(2) 第二種方法是代幣擁有者首先調(diào)用 approve,然后傳入與以上相同的參數(shù)树叽。接著尊剔,該合約會(huì)存儲(chǔ)誰(shuí)被允許提取代幣,通常存儲(chǔ)到一個(gè) mapping (uint256 => address) 里。然后须误,當(dāng)有人調(diào)用 takeOwnership 時(shí)挨稿,合約會(huì)檢查 msg.sender 是否得到擁有者的批準(zhǔn)來(lái)提取代幣,如果是京痢,則將代幣轉(zhuǎn)移給他奶甘。
繼承自多個(gè)合約:
contract SatoshiNakamoto is NickSzabo, HalFinney {
// 嘖嘖嘖,宇宙的奧秘泄露了
}
合約安全增強(qiáng): 溢出和下溢
為了防止這些情況祭椰,OpenZeppelin 建立了一個(gè)叫做 SafeMath 的 庫(kù)(library)臭家,默認(rèn)情況下可以防止這些問題。
一個(gè)庫(kù) 是 Solidity 中一種特殊的合約方淤。其中一個(gè)有用的功能是給原始數(shù)據(jù)類型增加一些方法钉赁。
比如,使用 SafeMath 庫(kù)的時(shí)候携茂,我們將使用 using SafeMath for uint256 這樣的語(yǔ)法你踩。 SafeMath 庫(kù)有四個(gè)方法 — add, sub讳苦, mul带膜, 以及 div。現(xiàn)在我們可以這樣來(lái)讓 uint256 調(diào)用這些方法:
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 調(diào)用這些方法了
uint test = 2;
test = test.mul(3); // test 等于 6 了
test = test.add(5); // test 等于 11 了
注意 mul 和 add 其實(shí)都需要兩個(gè)參數(shù)鸳谜。 在我們聲明了 using SafeMath for uint 后膝藕,我們用來(lái)調(diào)用這些方法的 uint 就自動(dòng)被作為第一個(gè)參數(shù)傳遞進(jìn)去了(在此例中就是 test)
assert 和 require 相似,若結(jié)果為否它就會(huì)拋出錯(cuò)誤咐扭。 assert 和 require 區(qū)別在于芭挽,require 若失敗則會(huì)返還給用戶剩下的 gas, assert 則不會(huì)蝗肪。所以大部分情況下袜爪,你寫代碼的時(shí)候會(huì)比較喜歡 require,assert 只在代碼可能出現(xiàn)嚴(yán)重錯(cuò)誤的時(shí)候使用穗慕,比如 uint 溢出。
第六章 應(yīng)用前端和 Web3.js
當(dāng)你想要調(diào)用一份智能合約的一個(gè)方法妻导,你需要從其中一個(gè)節(jié)點(diǎn)中查找并告訴它:
- 智能合約的地址
- 你想調(diào)用的方法逛绵,以及
- 你想傳入那個(gè)方法的參數(shù)
以太坊節(jié)點(diǎn)只能識(shí)別一種叫做 JSON-RPC 的語(yǔ)言。這種語(yǔ)言直接讀起來(lái)并不好懂倔韭。幸運(yùn)的是 Web3.js 把這些令人討厭的查詢語(yǔ)句都隱藏起來(lái)了术浪, 所以你只需要與方便易懂的 JavaScript 界面進(jìn)行交互即可。
CryptoZombies.methods.createRandomZombie("Vitalik Nakamoto ??")
.send({ from: "0xb60e8dd61c5d32be8058bb8eb970870f07233155", gas: "3000000" })
Web3 提供者
以太坊是由共享同一份數(shù)據(jù)的相同拷貝的 節(jié)點(diǎn) 構(gòu)成的寿酌。 在 Web3.js 里設(shè)置 Web3 的 Provider(提供者) 告訴我們的代碼應(yīng)該和 哪個(gè)節(jié)點(diǎn) 交互來(lái)處理我們的讀寫胰苏。這就好像在傳統(tǒng)的 Web 應(yīng)用程序中為你的 API 調(diào)用設(shè)置遠(yuǎn)程 Web 服務(wù)器的網(wǎng)址。
Infura 是一個(gè)服務(wù)醇疼,它維護(hù)了很多以太坊節(jié)點(diǎn)并提供了一個(gè)緩存層來(lái)實(shí)現(xiàn)高速讀取硕并。你可以用他們的 API 來(lái)免費(fèi)訪問這個(gè)服務(wù)法焰。 用 Infura 作為節(jié)點(diǎn)提供者,你可以不用自己運(yùn)營(yíng)節(jié)點(diǎn)就能很可靠地向以太坊發(fā)送倔毙、接收信息埃仪。
var web3 = new Web3(new Web3.providers.WebsocketProvider("wss://mainnet.infura.io/ws"));
這里是一些 Metamask 提供的示例代碼,用來(lái)檢查用戶是否安裝了MetaMask陕赃,如果沒有安裝就告訴用戶需要安裝MetaMask來(lái)使用我們的應(yīng)用卵蛉。
window.addEventListener('load', function() {
// 檢查web3是否已經(jīng)注入到(Mist/MetaMask)
if (typeof web3 !== 'undefined') {
// 使用 Mist/MetaMask 的提供者
web3js = new Web3(web3.currentProvider);
} else {
// 處理用戶沒安裝的情況, 比如顯示一個(gè)消息
// 告訴他們要安裝 MetaMask 來(lái)使用我們的應(yīng)用
}
// 現(xiàn)在你可以啟動(dòng)你的應(yīng)用并自由訪問 Web3.js:
startApp()
})
合約 ABI
ABI 意為應(yīng)用二進(jìn)制接口(Application Binary Interface)么库。 基本上傻丝,它是以 JSON 格式表示合約的方法,告訴 Web3.js 如何以合同理解的方式格式化函數(shù)調(diào)用诉儒。
實(shí)例化 Web3.js:
// 實(shí)例化 myContract
var myContract = new web3js.eth.Contract(myABI, myContractAddress);
調(diào)用和合約函數(shù)
Web3.js 有兩個(gè)方法來(lái)調(diào)用我們合約的函數(shù): call and send.
call 用來(lái)調(diào)用 view 和 pure 函數(shù)葡缰。它只運(yùn)行在本地節(jié)點(diǎn),不會(huì)在區(qū)塊鏈上創(chuàng)建事務(wù)允睹。
myContract.methods.myMethod(123).call()
send 將創(chuàng)建一個(gè)事務(wù)并改變區(qū)塊鏈上的數(shù)據(jù)运准。你需要用 send 來(lái)調(diào)用任何非 view 或者 pure 的函數(shù)。
myContract.methods.myMethod(123).send()
相對(duì) call 函數(shù)缭受,send 函數(shù)有如下主要區(qū)別:
- send 一個(gè)事務(wù)需要一個(gè) from 地址來(lái)表明誰(shuí)在調(diào)用這個(gè)函數(shù)(也就是你 Solidity 代碼里的 msg.sender )胁澳。 我們需要這是我們 DApp 的用戶,這樣一來(lái) MetaMask 才會(huì)彈出提示讓他們對(duì)事務(wù)簽名米者。
- send 一個(gè)事務(wù)將花費(fèi) gas
- 在用戶 send 一個(gè)事務(wù)到該事務(wù)對(duì)區(qū)塊鏈產(chǎn)生實(shí)際影響之間有一個(gè)不可忽略的延遲韭畸。這是因?yàn)槲覀儽仨毜却聞?wù)被包含進(jìn)一個(gè)區(qū)塊里,以太坊上一個(gè)區(qū)塊的時(shí)間平均下來(lái)是15秒左右蔓搞。如果當(dāng)前在以太坊上有大量掛起事務(wù)或者用戶發(fā)送了過低的 gas 價(jià)格胰丁,我們的事務(wù)可能需要等待數(shù)個(gè)區(qū)塊才能被包含進(jìn)去,往往可能花費(fèi)數(shù)分鐘喂分。
獲得 MetaMask中的用戶賬戶
MetaMask 允許用戶在擴(kuò)展中管理多個(gè)賬戶锦庸。
我們可以通過這樣來(lái)獲取 web3 變量中激活的當(dāng)前賬戶:
var userAccount = web3.eth.accounts[0]
檢查賬號(hào)是否切換:
var accountInterval = setInterval(function() {
// 檢查賬戶是否切換
if (web3.eth.accounts[0] !== userAccount) {
userAccount = web3.eth.accounts[0];
// 調(diào)用一些方法來(lái)更新界面
updateInterface();
}
}, 100);
在實(shí)際應(yīng)用中,你肯定想要在應(yīng)用中使用諸如 React 或 Vue.js 這樣的前端框架來(lái)讓你的前端開發(fā)變得輕松一些蒲祈。
function createRandomZombie(name) {
// 這將需要一段時(shí)間甘萧,所以在界面中告訴用戶這一點(diǎn)
// 事務(wù)被發(fā)送出去了
$("#txStatus").text("正在區(qū)塊鏈上創(chuàng)建僵尸,這將需要一會(huì)兒...");
// 把事務(wù)發(fā)送到我們的合約:
return cryptoZombies.methods.createRandomZombie(name)
.send({ from: userAccount })
.on("receipt", function(receipt) {
$("#txStatus").text("成功生成了 " + name + "!");
// 事務(wù)被區(qū)塊鏈接受了梆掸,重新渲染界面
getZombiesByOwner(userAccount).then(displayZombies);
})
.on("error", function(error) {
// 告訴用戶合約失敗了
$("#txStatus").text(error);
});
}
receipt 將在合約被包含進(jìn)以太坊區(qū)塊上以后被觸發(fā)扬卷,這意味著僵尸被創(chuàng)建并保存進(jìn)我們的合約了。
error 將在事務(wù)未被成功包含進(jìn)區(qū)塊后觸發(fā)酸钦,比如用戶未支付足夠的 gas怪得。我們需要在界面中通知用戶事務(wù)失敗以便他們可以再次嘗試。
調(diào)用 Payable 函數(shù)
一個(gè) wei 是以太的最小單位 — 1 ether 等于 10^18 wei
// 把 1 ETH 轉(zhuǎn)換成 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中的調(diào)用
cryptoZombies.events.NewZombie()
.on("data", function(event) {
let zombie = event.returnValues;
console.log("一個(gè)新僵尸誕生了!", zombie.zombieId, zombie.name, zombie.dna);
}).on('error', console.error);
注意這段代碼將在 任何 僵尸生成的時(shí)候激發(fā)一個(gè)警告信息——而不僅僅是當(dāng)前用用戶的僵尸徒恋。如果我們只想對(duì)當(dāng)前用戶發(fā)出提醒呢蚕断?
為了篩選僅和當(dāng)前用戶相關(guān)的事件,我們的 Solidity 合約將必須使用 indexed 關(guān)鍵字因谎,就像我們?cè)?ERC721 實(shí)現(xiàn)中的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;
// 當(dāng)前用戶更新了一個(gè)僵尸基括!更新界面來(lái)顯示
}).on('error', console.error);
因?yàn)槟憧梢杂眠@個(gè)方法來(lái)查詢從最開始起的事件日志,這就有了一個(gè)非常有趣的用例: 用事件來(lái)作為一種更便宜的存儲(chǔ)财岔。若你還能記得风皿,在區(qū)塊鏈上保存數(shù)據(jù)是 Solidity 中最貴的操作之一。但是用事件就便宜太多太多了匠璧。
這里的短板是桐款,事件不能從智能合約本身讀取。但是夷恍,如果你有一些數(shù)據(jù)需要永久性地記錄在區(qū)塊鏈中以便可以在應(yīng)用的前端中讀取魔眨,這將是一個(gè)很好的用例。這些數(shù)據(jù)不會(huì)影響智能合約向前的狀態(tài)酿雪。
總結(jié)
這里介紹了代幣的代碼以及相關(guān)的實(shí)現(xiàn)遏暴。以及web3的使用。都是一些很實(shí)用的內(nèi)容指黎。