作者介绍

Silver CEO 星际区块链信息发展有限公司

项目组件

  这个项目是一个构建在以太坊上的游戏,感谢这个团队给我们提供的案例:https://cryptozombies.io

  从功能的角度看,有如下脚本:

zombiefactory.sol:定义zombie和生成zombie。zombiefeeding.sol:定义小猫接口,给zombie吃小猫。zombieattack.sol:zombie打架的功能。erc721.sol:ERC721代币的接口。ownable.sol:用户认证的接口。safemath.sol:运算时安全检查。zombieownership.sol:zombie的所属功能,transfer,approve,takeOwnership等功能。zombiehelper.sol:zombie的辅助功能。该名字,改DNA,提升等级,设置levelUpFee,提现,查看僵尸军团等功能。index.html:前端交互调用。cryptozombies_abi.js:ABI文档。zombiefactory.sol

pragma solidity ^0.4.19;import "./ownable.sol";import "./safemath.sol";contract ZombieFactory is Ownable { using SafeMath for uint256; // 事件是合约和区块链通讯的一种机制。 // 当僵尸创造出来时,前端能监听到这个事件,并将它显示出来。 event NewZombie(uint zombieId, string name, uint dna); /** 僵尸的NDA只有16个字符 dnaModulus等于10^16,DNA可以用模运算符 % 把一个整数变成16位 冷却时间长达1天 **/ uint dnaDigits = 16; uint dnaModulus = 10 ** dnaDigits; uint cooldownTime = 1 days; // 使用struct节省存储空间,节约gas。readyTime实现“冷却定时器” struct Zombie { string name; uint dna; uint32 level; uint32 readyTime; uint16 winCount; uint16 lossCount; } // 公共可变长数组 Zombie[] public zombies; // 给数据库中的僵尸指定主人,支持多玩家模式 // mapping和address,通过僵尸id查到拥有者的address mapping (uint => address) public zombieToOwner; // 通过address查询到有多少只僵尸 mapping (address => uint) ownerZombieCount; // 内部方法以 _ 开头,函数里面的变量以 _ 开头,区别全局变量。 function _createZombie(string _name, uint _dna) internal { uint id = zombies.push(Zombie(_name, _dna, 1, uint32(now + cooldownTime), 0, 0)) - 1; // msg.sender 是合约当前调用者的address或者智能合约的address zombieToOwner[id] = msg.sender; ownerZombieCount[msg.sender]++; NewZombie(id, _name, _dna); } // 函数没有改变Solidity里的状态,没有改变任何值或者写任何东西,把函数定义为view, // 意味着只能读取数据不能更改数据 // keccak256是以太坊提供的SHA3散列函数,把一个字符串转换成一个256位的16进制数字。 // 只能用它造一个伪随机数。 function _generateRandomDna(string _str) private view returns (uint) { uint rand = uint(keccak256(_str)); return rand % dnaModulus; } function createRandomZombie(string _name) public { // require使函数在执行过程中,当不满足某些条件时抛出错误,并停止执行 require(ownerZombieCount[msg.sender] == 0); uint randDna = _generateRandomDna(_name); randDna = randDna - randDna % 100; _createZombie(_name, randDna); }}zombiefeeding.sol

pragma solidity ^0.4.19;import "./zombiefactory.sol";// 定义一个借口,使用contract关键字,在接口里定义getKitty函数contract KittyInterface { function getKitty(uint256 _id) external view returns ( bool isGestating, bool isReady, uint256 cooldownIndex, uint256 nextActionAt, uint256 siringWithId, uint256 birthTime, uint256 matronId, uint256 sireId, uint256 generation, uint256 genes );}// 继承ZombieFactory合约contract ZombieFeeding is ZombieFactory { KittyInterface kittyContract; // modifier关键字告诉编译器,这是个modifier修饰符,而不是function,不能像函数直接调用,只能添加到函数定义的末尾,用以改变函数的行为 modifier onlyOwnerOf(uint _zombieId) { require(msg.sender == zombieToOwner[_zombieId]); _; } // external 函数只能在合约之外调用,不能被合约内的其他函数调用 // 通过程序更改CryptoKities合约地址 // onlyOwner是指定合约的所有权,指定一个主人,只有主人(合约部署者)对它享有特权 function setKittyContractAddress(address _address) external onlyOwner { kittyContract = KittyInterface(_address); } // internal 合约可访问父合约中定义的内部函数 function _triggerCooldown(Zombie storage _zombie) internal { _zombie.readyTime = uint32(now + cooldownTime); } function _isReady(Zombie storage _zombie) internal view returns (bool) { return (_zombie.readyTime <= now); } function feedAndMultiply(uint _zombieId, uint _targetDna, string _species) internal onlyOwnerOf(_zombieId) { // storage变量永久存储在区块链中 Zombie storage myZombie = zombies[_zombieId]; require(_isReady(myZombie)); _targetDna = _targetDna % dnaModulus; uint newDna = (myZombie.dna + _targetDna) / 2; // 如果是kitty变过来的,用99替换新僵尸DNA的最后两位数字 if (keccak256(_species) == keccak256("kitty")) { newDna = newDna - newDna % 100 + 99; } _createZombie("NoName", newDna); _triggerCooldown(myZombie); } function feedOnKitty(uint _zombieId, uint _kittyId) public { uint kittyDna; (,,,,,,,,,kittyDna) = kittyContract.getKitty(_kittyId); feedAndMultiply(_zombieId, kittyDna, "kitty"); }}zombieattack.sol

pragma solidity ^0.4.19;import "./zombiehelper.sol";/** 僵尸战斗功能,继承自ZombieHelper**/contract ZombieAttack is ZombieHelper { uint randNonce = 0; uint attackVictoryProbability = 70; // 拿now,msg.sender,自增的nonce,转成一个哈希值,再转uint,% 100 取最后两位,生成一个0到100的随机数 // 当然最后拿到的是一个伪随机数,但在具体的项目中,不会吸引×××者来×××就是现实安全的。 function randMod(uint _modulus) internal returns(uint) { randNonce++; return uint(keccak256(now, msg.sender, randNonce)) % _modulus; } // 选择自己的僵尸,选择对手的一个僵尸去××× // ×××方有70%的获胜概率 // 所有的僵尸都有一个winCount和lossCount,记录输赢 // ×××方获胜,僵尸升级并产生一个新僵尸,成功次数累加1 // ×××方失败,失败次数累加1 // 无论输赢,当前僵尸的冷却时间将被激活 function attack(uint _zombieId, uint _targetId) external onlyOwnerOf(_zombieId) { Zombie storage myZombie = zombies[_zombieId]; Zombie storage enemyZombie = zombies[_targetId]; uint rand = randMod(100); if (rand <= attackVictoryProbability) { myZombie.winCount++; myZombie.level++; enemyZombie.lossCount++; feedAndMultiply(_zombieId, enemyZombie.dna, "zombie"); } else { myZombie.lossCount++; enemyZombie.winCount++; _triggerCooldown(myZombie); } }}erc721.sol

/** ERC20代币,一个代币只是一个追踪谁拥有多少该代币的合约,和一些可以让那些用户将它们的代币转移到其他地址的函数 ERC721 适合CryptpZombies这样的加密收藏品,是ERC721代币 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;}ownable.sol

pragma solidity ^0.4.19;/** * @title Ownable * @dev The Ownable contract has an owner address, and provides basic authorization control * functions, this simplifies the implementation of "user permissions". */contract Ownable { address public owner; event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); /** * @dev The Ownable constructor sets the original `owner` of the contract to the sender * account. */ function Ownable() public { owner = msg.sender; } /** * @dev Throws if called by any account other than the owner. */ modifier onlyOwner() { require(msg.sender == owner); _; } /** * @dev Allows the current owner to transfer control of the contract to a newOwner. * @param newOwner The address to transfer ownership to. */ function transferOwnership(address newOwner) public onlyOwner { require(newOwner != address(0)); OwnershipTransferred(owner, newOwner); owner = newOwner; }}safemath.sol

pragma solidity ^0.4.18;/** * @title SafeMath * @dev Math operations with safety checks that throw on error */library SafeMath { /** * @dev Multiplies two numbers, throws on overflow. */ 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; } /** * @dev Integer division of two numbers, truncating the quotient. */ 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; } /** * @dev Subtracts two numbers, throws on overflow (i.e. if subtrahend is greater than minuend). */ function sub(uint256 a, uint256 b) internal pure returns (uint256) { assert(b <= a); return a - b; } /** * @dev Adds two numbers, throws on overflow. */ function add(uint256 a, uint256 b) internal pure returns (uint256) { uint256 c = a + b; assert(c >= a); return c; }}zombieownership.sol

pragma solidity ^0.4.19;import "./zombieattack.sol";import "./erc721.sol";import "./safemath.sol";contract ZombieOwnership is ZombieAttack, ERC721 { using SafeMath for uint256; mapping (uint => address) zombieApprovals; // 传入address函数,返回address拥有多少ERC721代币,整数不可分割 function balanceOf(address _owner) public view returns (uint256 _balance) { return ownerZombieCount[_owner]; } // 传入一个代币ID,也是僵尸ID,返回该代币拥有者的address function ownerOf(uint256 _tokenId) public view returns (address _owner) { return zombieToOwner[_tokenId]; } // 代币的拥有者调用transfer方法,传入需要转移到的address和他想转移的代币的_tokenId。 // 合约安全增强:溢出和下溢 // 溢出(overflow),uint8的变量,存储的最大数就是二进制11111111,也就是十进制的255,加1就出现溢出。 // 下溢(underflow),一个等于0的uint8,减去1就出现下溢,变成255,uint是无符号的,不能等于复数。 function _transfer(address _from, address _to, uint256 _tokenId) private { ownerZombieCount[_to] = ownerZombieCount[_to].add(1); ownerZombieCount[msg.sender] = ownerZombieCount[msg.sender].sub(1); zombieToOwner[_tokenId] = _to; Transfer(_from, _to, _tokenId); } // 都有一个修饰符onlyOwnerOf function transfer(address _to, uint256 _tokenId) public onlyOwnerOf(_tokenId) { _transfer(msg.sender, _to, _tokenId); } // 代币的拥有者调用approve,传入允许提取代币的address和允许提取的代币_tokenId。 function approve(address _to, uint256 _tokenId) public onlyOwnerOf(_tokenId) { zombieApprovals[_tokenId] = _to; Approval(msg.sender, _to, _tokenId); } // require检查确保msg.sender已经被批准来提取这个代币,也就是僵尸。 function takeOwnership(uint256 _tokenId) public { require(zombieApprovals[_tokenId] == msg.sender); address owner = ownerOf(_tokenId); _transfer(owner, msg.sender, _tokenId); }}zombiehelper.sol

pragma solidity ^0.4.19;import "./zombiefeeding.sol";/** 辅助方法**/contract ZombieHelper is ZombieFeeding { uint levelUpFee = 0.001 ether; // 修饰符modifier的最后一行为 _ ,表示修饰符调用结束后返回,并执行调用函数余下的部分。 modifier aboveLevel(uint _level, uint _zombieId) { require(zombies[_zombieId].level >= _level); _; } // 提现功能,只能是合约主人享有 // 通过transfer函数向一个地址发送以太 function withdraw() external onlyOwner { owner.transfer(this.balance); } // 合约主人设置levelUpFee function setLevelUpFee(uint _fee) external onlyOwner { levelUpFee = _fee; } // payable修饰符,是一种可以接收以太的特殊函数 // 通过支付ETH来升级僵尸,ETH将存储在你拥有的合约中 function levelUp(uint _zombieId) external payable { require(msg.value == levelUpFee); zombies[_zombieId].level++; } // 2级以上的僵尸可以改名 function changeName(uint _zombieId, string _newName) external aboveLevel(2, _zombieId) onlyOwnerOf(_zombieId) { zombies[_zombieId].name = _newName; } // 20级以上的僵尸可以定制DNA function changeDna(uint _zombieId, uint _newDna) external aboveLevel(20, _zombieId) onlyOwnerOf(_zombieId) { zombies[_zombieId].dna = _newDna; } // 查看某个某家的整个僵尸军团,只需要从区块链中读取数据,view函数。 function getZombiesByOwner(address _owner) external view returns(uint[]) { // memory 关键字是使用临时存储,调用完就释放 uint[] memory result = new uint[](ownerZombieCount[_owner]); uint counter = 0; for (uint i = 0; i < zombies.length; i++) { if (zombieToOwner[i] == _owner) { result[counter] = i; counter++; } } return result; }}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; // Web3.js需要合约地址和ABI来和合约进行对话 // 编译了ABI放在cryptozobimes_abi.js文件中,保存为一个名为cryptoZombiesABI的变量中 // Web3.js有两个方法来调用合约的函数:call和send // call用来调用view和pure函数,只运行在本地节点,不会在区块链上创建事务 // send将创建一个事务并改变区块链上的数据,用send调用非view和pure的函数 function startApp() { var cryptoZombiesAddress = "YOUR_CONTRACT_ADDRESS"; cryptoZombies = new web3js.eth.Contract(cryptoZombiesABI, cryptoZombiesAddress); // 在MetaMask中切换账户,应用需要监控这个变量,通过setInterval方法来实现 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); // Web3 provider说明跟哪个节点进行交互处理我们的读写。 // Infura是一个服务,维护了大量以太坊节点并提供一个缓存层来实现高速读取。 var web3Infura = new Web3(new Web3.providers.WebsocketProvider("wss://mainnet.infura.io/ws")); var czEvents = new web3Infura.eth.Contract(cryptoZombiesABI, cryptoZombiesAddress); // 任何僵尸生成的时候激发一个警告信息, // 如果只想监听对当前用户的提醒呢,使用indexed关键字 // 例如 event Transfer(address indexed _from, address indexed _to, uint256 _tokenId); // _from 和 _to 都是indexed,可在前端事件监听中过滤事件 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>cryptozombies_abi.js

var cryptozombiesABI = [ { "constant": false, "inputs": [ { "name": "_to", "type": "address" }, { "name": "_tokenId", "type": "uint256" } ], "name": "approve", "outputs": [], "payable": false, "stateMutability": "nonpayable", "type": "function" }, { "constant": false, "inputs": [ { "name": "_zombieId", "type": "uint256" } ], "name": "levelUp", "outputs": [], "payable": true, "stateMutability": "payable", "type": "function" }, { "constant": false, "inputs": [ { "name": "_zombieId", "type": "uint256" }, { "name": "_kittyId", "type": "uint256" } ], "name": "feedOnKitty", "outputs": [], "payable": false, "stateMutability": "nonpayable", "type": "function" }, { "constant": true, "inputs": [ { "name": "", "type": "uint256" } ], "name": "zombies", "outputs": [ { "name": "name", "type": "string" }, { "name": "dna", "type": "uint256" }, { "name": "level", "type": "uint32" }, { "name": "readyTime", "type": "uint32" }, { "name": "winCount", "type": "uint16" }, { "name": "lossCount", "type": "uint16" } ], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": false, "inputs": [], "name": "withdraw", "outputs": [], "payable": false, "stateMutability": "nonpayable", "type": "function" }, { "constant": true, "inputs": [ { "name": "_owner", "type": "address" } ], "name": "getZombiesByOwner", "outputs": [ { "name": "", "type": "uint256[]" } ], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": true, "inputs": [ { "name": "", "type": "uint256" } ], "name": "zombieToOwner", "outputs": [ { "name": "", "type": "address" } ], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": false, "inputs": [ { "name": "_address", "type": "address" } ], "name": "setKittyContractAddress", "outputs": [], "payable": false, "stateMutability": "nonpayable", "type": "function" }, { "constant": false, "inputs": [ { "name": "_zombieId", "type": "uint256" }, { "name": "_newDna", "type": "uint256" } ], "name": "changeDna", "outputs": [], "payable": false, "stateMutability": "nonpayable", "type": "function" }, { "constant": true, "inputs": [ { "name": "_tokenId", "type": "uint256" } ], "name": "ownerOf", "outputs": [ { "name": "_owner", "type": "address" } ], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": true, "inputs": [ { "name": "_owner", "type": "address" } ], "name": "balanceOf", "outputs": [ { "name": "_balance", "type": "uint256" } ], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": false, "inputs": [ { "name": "_name", "type": "string" } ], "name": "createRandomZombie", "outputs": [], "payable": false, "stateMutability": "nonpayable", "type": "function" }, { "constant": true, "inputs": [], "name": "owner", "outputs": [ { "name": "", "type": "address" } ], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": false, "inputs": [ { "name": "_to", "type": "address" }, { "name": "_tokenId", "type": "uint256" } ], "name": "transfer", "outputs": [], "payable": false, "stateMutability": "nonpayable", "type": "function" }, { "constant": true, "inputs": [], "name": "getAllZombies", "outputs": [ { "name": "", "type": "uint256[]" } ], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": false, "inputs": [ { "name": "_tokenId", "type": "uint256" } ], "name": "takeOwnership", "outputs": [], "payable": false, "stateMutability": "nonpayable", "type": "function" }, { "constant": false, "inputs": [ { "name": "_zombieId", "type": "uint256" }, { "name": "_newName", "type": "string" } ], "name": "changeName", "outputs": [], "payable": false, "stateMutability": "nonpayable", "type": "function" }, { "constant": false, "inputs": [ { "name": "_fee", "type": "uint256" } ], "name": "setLevelUpFee", "outputs": [], "payable": false, "stateMutability": "nonpayable", "type": "function" }, { "constant": false, "inputs": [ { "name": "_zombieId", "type": "uint256" }, { "name": "_targetId", "type": "uint256" } ], "name": "attack", "outputs": [], "payable": false, "stateMutability": "nonpayable", "type": "function" }, { "constant": false, "inputs": [ { "name": "newOwner", "type": "address" } ], "name": "transferOwnership", "outputs": [], "payable": false, "stateMutability": "nonpayable", "type": "function" }, { "anonymous": false, "inputs": [ { "indexed": true, "name": "_from", "type": "address" }, { "indexed": true, "name": "_to", "type": "address" }, { "indexed": false, "name": "_tokenId", "type": "uint256" } ], "name": "Transfer", "type": "event" }, { "anonymous": false, "inputs": [ { "indexed": true, "name": "_owner", "type": "address" }, { "indexed": true, "name": "_approved", "type": "address" }, { "indexed": false, "name": "_tokenId", "type": "uint256" } ], "name": "Approval", "type": "event" }, { "anonymous": false, "inputs": [ { "indexed": false, "name": "attackResult", "type": "bool" }, { "indexed": false, "name": "winCount", "type": "uint16" }, { "indexed": false, "name": "lossCount", "type": "uint16" } ], "name": "AttackResult", "type": "event" }, { "anonymous": false, "inputs": [ { "indexed": false, "name": "zombieId", "type": "uint256" }, { "indexed": false, "name": "name", "type": "string" }, { "indexed": false, "name": "dna", "type": "uint256" } ], "name": "NewZombie", "type": "event" }, { "anonymous": false, "inputs": [ { "indexed": true, "name": "previousOwner", "type": "address" }, { "indexed": true, "name": "newOwner", "type": "address" } ], "name": "OwnershipTransferred", "type": "event" }] 文章来源:虫洞社区(https://www.uzanapp.com/ )