Skip to content

Latest commit

 

History

History
624 lines (513 loc) · 31.5 KB

07.md

File metadata and controls

624 lines (513 loc) · 31.5 KB
title actions requireLogin material
การส่ง Transactions
checkAnswer
hints
true
editor
language startingCode answer
html
index.html zombieownership.sol zombieattack.sol zombiehelper.sol zombiefeeding.sol zombiefactory.sol ownable.sol safemath.sol erc721.sol
<!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() { // ตรวจสอบว่าบัญชีได่มีการเปลี่ยนแปลงไปหรือไม่ if (web3.eth.accounts[0] !== userAccount) { userAccount = web3.eth.accounts[0]; // เรียกใช้ฟังก์ชั่นที่จะทำการอัพเดท UI ด้วยบัญชีใหม่ getZombiesByOwner(userAccount) .then(displayZombies); } }, 100); } function displayZombies(ids) { $("#zombies").empty(); for (id of ids) { // เข้าดูข้อมูลของซอมบี้จาก contract ของเราเอง รีเทิร์นอ็อบเจ็กต์ `zombie` ออกมา getZombieDetails(id) .then(function(zombie) { // ใช้ "template literals" ของ ES6 ในการเพิ่มตัวแปรเข้าไปยังไฟล์ HTML // Append เพิ่มแต่ละอย่างเข้าไปที่ #zombies div $("#zombies").append(`<div class="zombie"> <ul> <li>Name: ${zombie.name}&lt;/li&gt; &lt;li&gt;DNA: ${zombie.dna}</li> <li>Level: ${zombie.level}&lt;/li&gt; &lt;li&gt;Wins: ${zombie.winCount}</li> <li>Losses: ${zombie.lossCount}&lt;/li&gt; &lt;li&gt;Ready Time: ${zombie.readyTime}</li> </ul> </div>`); }); } } // เริ่มที่ตรงนี้ 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() { // ตรวจสอบว่าbrowser ได้เพิ่ม Web3 เข้ามาแล้วหรือยัง (Mist/MetaMask) if (typeof web3 !== 'undefined') { // ใช้ Mist/MetaMask's provider web3js = new Web3(web3.currentProvider); } else { // รับมือกับกรณีที่ผู้ใช้ยังไม่ได้ทำการ install เจ้า Metamask // เป็นไปได้ว่าเราอาจจะต้องแสดงข้อความเพื่อบอกผู้ใช้ให้ลง Metamask เสียก่อน } // ตอนนี้เราก็สามารถที่จะเปิดแอพพลิเคชั่นขึ้นมา และเข้าถึง web3 ได้อย่างอิสระแล้ว: startApp() }) </script> </body> </html>
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; function balanceOf(address _owner) public view returns (uint256 _balance) { return ownerZombieCount[_owner]; } function ownerOf(uint256 _tokenId) public view returns (address _owner) { return zombieToOwner[_tokenId]; } 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); } function transfer(address _to, uint256 _tokenId) public onlyOwnerOf(_tokenId) { _transfer(msg.sender, _to, _tokenId); } function approve(address _to, uint256 _tokenId) public onlyOwnerOf(_tokenId) { zombieApprovals[_tokenId] = _to; Approval(msg.sender, _to, _tokenId); } function takeOwnership(uint256 _tokenId) public { require(zombieApprovals[_tokenId] == msg.sender); address owner = ownerOf(_tokenId); _transfer(owner, msg.sender, _tokenId); } }
pragma solidity ^0.4.19; import "./zombiehelper.sol"; contract ZombieAttack is ZombieHelper { uint randNonce = 0; uint attackVictoryProbability = 70; function randMod(uint _modulus) internal returns(uint) { randNonce++; return uint(keccak256(now, msg.sender, randNonce)) % _modulus; } 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); } } }
pragma solidity ^0.4.19; import "./zombiefeeding.sol"; contract ZombieHelper is ZombieFeeding { uint levelUpFee = 0.001 ether; modifier aboveLevel(uint _level, uint _zombieId) { require(zombies[_zombieId].level >= _level); _; } function withdraw() external onlyOwner { owner.transfer(this.balance); } function setLevelUpFee(uint _fee) external onlyOwner { levelUpFee = _fee; } function levelUp(uint _zombieId) external payable { require(msg.value == levelUpFee); zombies[_zombieId].level++; } function changeName(uint _zombieId, string _newName) external aboveLevel(2, _zombieId) onlyOwnerOf(_zombieId) { zombies[_zombieId].name = _newName; } function changeDna(uint _zombieId, uint _newDna) external aboveLevel(20, _zombieId) onlyOwnerOf(_zombieId) { zombies[_zombieId].dna = _newDna; } function getZombiesByOwner(address _owner) external view returns(uint[]) { 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; } }
pragma solidity ^0.4.19; import "./zombiefactory.sol"; 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 ); } contract ZombieFeeding is ZombieFactory { KittyInterface kittyContract; modifier onlyOwnerOf(uint _zombieId) { require(msg.sender == zombieToOwner[_zombieId]); _; } function setKittyContractAddress(address _address) external onlyOwner { kittyContract = KittyInterface(_address); } 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) { Zombie storage myZombie = zombies[_zombieId]; require(_isReady(myZombie)); _targetDna = _targetDna % dnaModulus; uint newDna = (myZombie.dna + _targetDna) / 2; 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"); } }
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); uint dnaDigits = 16; uint dnaModulus = 10 ** dnaDigits; uint cooldownTime = 1 days; struct Zombie { string name; uint dna; uint32 level; uint32 readyTime; uint16 winCount; uint16 lossCount; } Zombie[] public zombies; mapping (uint => address) public zombieToOwner; 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; zombieToOwner[id] = msg.sender; ownerZombieCount[msg.sender]++; NewZombie(id, _name, _dna); } function _generateRandomDna(string _str) private view returns (uint) { uint rand = uint(keccak256(_str)); return rand % dnaModulus; } function createRandomZombie(string _name) public { require(ownerZombieCount[msg.sender] == 0); uint randDna = _generateRandomDna(_name); randDna = randDna - randDna % 100; _createZombie(_name, randDna); } }
/** * @title Ownable * @dev Ownable contract มี address ของ owner และได้เตรียมฟังก์ชั่นเกี่ยวกับการควบคุมขั้นพื้นฐานในเรื่องของ authorization ไว้ให้แล้ว * โดยส่วนนี้จะหมายถึงของการให้ "user permissions" หรือการให้สิทธิ์ในการเข้าถึงแก้ผู้ใช้นั่นเอง */ contract Ownable { address public owner; event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); /** * @dev Ownable constructor ตั้งค่าบัญชีของ sender ให้เป็น `owner` ดั้งเดิมของ contract * account */ function Ownable() public { owner = msg.sender; } /** * @dev Throw หากโดนเรียกโดยบัญชีใดก็ตามที่ไม่ใช่ owner ตัวเอง */ modifier onlyOwner() { require(msg.sender == owner); _; } /** * @dev อนุญาตให้ owner คนปัจจุบันสามารถยกการควบคุมดูแล contract ให้แก่ newOwner ได้ * @param newOwner คือ address ที่จะได้รับ ownership ไปนั่นเอง */ function transferOwnership(address newOwner) public onlyOwner { require(newOwner != address(0)); OwnershipTransferred(owner, newOwner); owner = newOwner; } }
pragma solidity ^0.4.18; /** * @title SafeMath * @dev Math operations ที่มาพร้อมกับ safety checks ในการ throw error */ library SafeMath { /** * @dev คูณเลข 2 จำนวนเข้าด้วยกัน จากนั้นก็ throw 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 2 จำนวน แบบไม่เอาผลหาร */ function div(uint256 a, uint256 b) internal pure returns (uint256) { // assert(b > 0); // Solidity จะ throw โดยอัตโนมัติหากเป็นกรณีที่หารด้วย 0 uint256 c = a / b; // assert(a == b * c + a % b); // There is no case in which this doesn't hold return c; } /** * @dev ลบเลข 2 จำนวน จากนั้นก็ throw overflow (ตัวอย่าง กรณีตัวลบมากกว่าตัวหาร) */ function sub(uint256 a, uint256 b) internal pure returns (uint256) { assert(b <= a); return a - b; } /** * @dev บวก 2 จำนวนเข้าด้วยกัน และ throw overflow */ function add(uint256 a, uint256 b) internal pure returns (uint256) { uint256 c = a + b; assert(c >= a); return c; } }
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; }
<!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() { // ดูว่าได้มีการเปลี่ยนบัญชีไปหรือไม่ if (web3.eth.accounts[0] !== userAccount) { userAccount = web3.eth.accounts[0]; // เรียกฟังก์ชั่นเพื่อทำการอัพเดทส่วน UI เพื่อดึงบัญชีใหม่ออกมาแสดงผล getZombiesByOwner(userAccount) .then(displayZombies); } }, 100); } function displayZombies(ids) { $("#zombies").empty(); for (id of ids) { // เป็นการดูข้อมูลของซอมบี้จาก contract และ return ออบเจ็กต์ `zombie` ออกมา getZombieDetails(id) .then(function(zombie) { // เป็นการใช้ "template literals" ของ ES6 ในการเพิ่มตัวแปรลงไปยังไฟล์ HTML // Append เข้าไปยัง #zombies div ทีละตัว $("#zombies").append(`<div class="zombie"> <ul> <li>Name: ${zombie.name}&lt;/li&gt; &lt;li&gt;DNA: ${zombie.dna}</li> <li>Level: ${zombie.level}&lt;/li&gt; &lt;li&gt;Wins: ${zombie.winCount}</li> <li>Losses: ${zombie.lossCount}&lt;/li&gt; &lt;li&gt;Ready Time: ${zombie.readyTime}</li> </ul> </div>`); }); } } function createRandomZombie(name) { // เราอาจต้องใช้เวลากับตรงนี้สักเล็กน้อย ดังนั้นให้เราอัพเดต UI เพื่อบอกผู้ใช้ให้รู้ด้วยว่า // transaction ได้ถูกส่งไปแล้ว $("#txStatus").text("Creating new zombie on the blockchain. This may take a while..."); // ส่ง tx ไปยัง contract ของเรา: return CryptoZombies.methods.createRandomZombie(name) .send({ from: userAccount }) .on("receipt", function(receipt) { $("#txStatus").text("Successfully created " + name + "!"); // Block chain ยอมรับ transaction เข้ามาเป็นที่เรียบร้อย ได้เวลามาเขียน UI ใหม่กันแล้ว getZombiesByOwner(userAccount).then(displayZombies); }) .on("error", function(error) { // เตือนผู้ใช้ว่า transaction ของพวกเขานั้นไม่ประสบความสำเร็จ $("#txStatus").text(error); }); } function feedOnKitty(zombieId, kittyId) { // อาจต้องใช้เวลากับตรงนี้เยอะพอสมควร ดังนั้นให้เราอัพเดต UI เพื่อบอกผู้ใช้ให้รู้ด้วยว่า // transaction ได้ถูกส่งไปแล้ว $("#txStatus").text("Eating a kitty. This may take a while..."); // ส่ง tx ไปยัง contract ของเรา: return CryptoZombies.methods.feedOnKitty(zombieId, KittyId) .send({ from: userAccount }) .on("receipt", function(receipt) { $("#txStatus").text("Ate a kitty and spawned a new Zombie!"); // Block chain ยอมรับ transaction เข้ามาเป็นที่เรียบร้อย มาเขียน UI ใหม่กัน getZombiesByOwner(userAccount).then(displayZombies); }) .on("error", function(error) { // เตือนผู้ใช้ว่า transaction ของพวกเขานั้นไม่ประสบความสำเร็จ $("#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() { // ตรวจสอบว่า browser ได้เพิ่ม Web3 เข้ามาแล้วหรือยัง (Mist/MetaMask) if (typeof web3 !== 'undefined') { // ใช้ Mist/MetaMask's provider web3js = new Web3(web3.currentProvider); } else { // รับมือกับกรณีที่ผู้ใช้ยังไม่ได้ทำการ install เจ้า Metamask // เป็นไปได้ว่าเราอาจจะต้องแสดงข้อความเพื่อบอกผู้ใช้ให้ลง Metamask เสียก่อน } // ตอนนี้เราก็สามารถที่จะเปิดแอพพลิเคชั่นขึ้นมา และเข้าถึง web3 ได้อย่างอิสระแล้ว: startApp() }) </script> </body> </html>

สุดยอด! ตอนนี้ UI ของเราก็สามารถตรวจสอบบัญชี metamask ของผู้ใช้ได้แล้ว และสามารถแสดงกองทัพซอมบี้ออกมาโดยอัตโนมัติผ่านทาง homepage ด้วยนะ

ตอนนี้ให้มาดูการใช้ฟังก์ชั่น send ในการเปลี่ยนข้อมูลบน smart contract ของเรากันดีกว่า

ซึ่งฟังก์ชั่นนี้ก็มีข้อแตกต่างจากฟังก์ชั่น call อยู่บางเรื่อง:

  1. การ send transaction นั้นต้องการที่อยู่ส่วน from เพื่อจะบอกได้ว่าใครเป็นคนที่เรียกใช้ฟังก์ชั่นนั้นขึ้นมา (ซึ่งจะกลายเป็น msg.sender ในโค้ด Solidity ของเรา) เนื่องจากต้องการให้ส่วนนี้เป็นผู้ใช้ DApp ที่ได้สร้างขึ้นมา เราจึงจะให้มี pop-up ของ MetaMask ขึ้นมาเพื่อบอกให้ผู้ใช้ส่ง transaction ไป

  2. การ send ค่า transaction costs gas

  3. ซึ่งเมื่อผู้ใช้ทำการ send transaction รวมทั้งเวลาที่ transaction เริ่มมีหน้าที่บน blockchain ก็อาจจะมีเหตุการณ์ที่ล่าช้าเกิดขึ้นอย่างมีสาเหตุ นั่นก็คือเพราะเราต้องรอให้ transaction ถูกรวมเข้าไปใน block เสียก่อน และนอกจากนี้ส่วน block time ของ Ethereum ก็มีค่าอยู่ที่ 15 วินาทีอีกด้วย แปลว่าหากเรามี transaction ที่รออยู่เป็นจำนวนมากบน Ethereum หรือผู้ใช้จ่าย gas price ที่ต่ำเกินไป transaction นี้ก็อาจจะต้องรอนานมากขึ้นกว่าเดิมในการส่งไปพิจารณา

แอพพลิเคชั่นนี้จึงต้องมีกลไกบางอย่างเพื่อรับมือธรรมชาติของโค้ดที่ asynchronous กันเช่นนี้

การประกอบร่างซอมบี้ขึ้นมา

ลองมาดูตัวอย่างของฟังก์ชั่นแรกใน contract ที่ผู้ใช้จะเรียก: createRandomZombie.

นี่คือโค้ด Solidity ภายใน contract ของเรา:

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) {
  // โดยตรงนี้อาจจะต้องใช้เวลาเล็กน้อย ดังนั้นเราจึงต้องอัพเดทให้ UI มีการบอกผู้ใช้ด้วยว่า
  // transaction ได้ถูกส่งไปแล้วเรียบร้อย
  $("#txStatus").text("Creating new zombie on the blockchain. This may take a while...");
  // ส่ง tx ไปยัง contract ของเรา:
  return CryptoZombies.methods.createRandomZombie(name)
  .send({ from: userAccount })
  .on("receipt", function(receipt) {
    $("#txStatus").text("Successfully created " + name + "!");
    // Blockchain ยอมรับ transaction ให้เข้ามาแล้ว ได้เวลาของการขียน UI ขึ้นมาใหม่
    getZombiesByOwner(userAccount).then(displayZombies);
  })
  .on("error", function(error) {
    // ให้เตือนผู้ใช้ว่า transaction ของพวกเขาเกิดความล้มเหลว
    $("#txStatus").text(error);
  });
}

ฟังก์ชั่นของเราส่ง หรือ send transaction ไปยัง Web3 provider แล้วพ่วง event listener เข้ามาด้วยดังนี้:

  • receipt จะออกมาเมื่อ transaction ถูกรับเข้ามาใน block ของ Ethereum ซึ่งแปลว่าซอมบี้ของเราได้ถูกสร้างขึ้นมาและเก็บลงใน contract เป็นที่เรียบร้อย
  • error จะ fire ออกมาถ้ามีปัญหาใดๆ ก็ตามที่ขัดขวางการ transaction จากการถูกเก็บลงใน block ยกตัวอย่างเช่น ผู้ใช้ไม่ได้ให้ gas ในปริมาณที่ได้ถูกกำหนดไว้ แปลว่าเราต้องบอกผู้ใช้ใน UI ว่า transaction จะไม่สำเร็จเพื่อที่พวกเขาจะได้ลองใหม่อีกครั้ง

Note: เรายังสามารถเพิ่ม gas และ gasPrice ได้ตามใจเมื่อเรียก send ขึ้นมา ยกตัวอย่างเช่น .send({ from: userAccount, gas: 3000000 }) ซึ่งถ้าเราไม่ได้กำหนดตรงส่วนนี้ MetaMask จะให้ผู้ใช้เป็นคนเลือกเอง

ลองมาทดสอบกันดู

div พร้อมกับ ID txStatus ได้ถูกใส่ลงมาเป็นที่เรียบร้อย — วิธีการนี้จะทำให้สามารถใช้ div ในการอัพเดทผู้ใช้ผ่านทางข้อความเกี่ยวกับสถานะของ transaction

  1. ด้านล่าง displayZombies ให้ copy / paste ตัวโค้ดจาก createRandomZombie ทางด้านบน

  2. ต่อมาก็ให้โค้ดฟังก์ชั่นอีกตัวนึงซึ่งก็คือ: feedOnKitty

logic ในการเรียกฟังก์ชั่น feedOnKitty ก็ค่อนข้างมีความใกล้เคียงกันกับตัวด้านบน — เราจะส่ง transaction ที่เรียกฟังก์ชั่นออกมา และ transaction ที่สำเร็จจะทำให้สามารถสร้างซอมบี้ขึ้นมาสำเร็จให้เรา ดังนั้นเดี๋ยวเราก็จะต้องมาวาด UI กันใหม่หลังจากนี้

สร้างก็อปปี้ของ createRandomZombie ลงด้านล่างของมัน แต่เปลี่ยนแปลงส่วนต่างๆ ดังนี้:

a) เรียกใช้ฟังก์ชั่นที่ 2 feedOnKitty ซึ่งจะรับ argument จำนวน 2 ตัว: zombieId และ kittyId

b) ข้อความ #txStatus ควรได้รับการอัพเดทให้เป็น: "กำลังเขมือบเจ้าแมว kitty อาจะใช้เวลาสักครู่..."

c) ให้มันเรียก feedOnKitty บน contract ของเราและส่ง argument เดิมจำนวน 2 ตัว

d) ข้อความที่สำเร็จบน #txStatus ควรมีใจความว่า: "เขมือบแมวน้อยเข้าไปและได้ซอมบี้ตัวใหม่ขึ้นเรียบร้อย!"