Skip to content

Latest commit

 

History

History
426 lines (340 loc) · 18.6 KB

01.md

File metadata and controls

426 lines (340 loc) · 18.6 KB
title actions requireLogin material
บทเริ่มต้นเกี่ยวกับ Web3.js
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> <!-- Include web3.js here --> </head> <body> </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 * */ 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> </head> <body> </body> </html>

หลังจบบทที่ 5 กันไป zombie DApp ของเราในตอนนี้ก็ถือว่าค่อนข้างดูดีพอสมควร ต่อไปนี้จะถึงช่วงของการสร้างเว็บเพจอย่างง่ายเพื่อให้ผู้ใช้สามารถ interact กับมันได้นั่นเอง

ใช่แล้ว เรากำลังจะสร้างมันขึ้นมาโดยการใช้ library ของ JavaScript จาก the Ethereum Foundation ที่เรียกว่า Web3.js กัน

Web3.js คืออะไรกันล่ะ?

จำได้ไหมว่า Ethereum network ถูกสร้างขึ้นมาจาก node หลายๆ อัน ซึ่งประกอบไปด้วยส่วน copy ของ blockchain หากเราต้องการเรียกฟังก์ชั่นบน smart contract ก็จะต้องค้นหาหนึ่ง node จากทั้งหมดออกมาและใส่ข้อมูลให้มันเกี่ยวกับเรื่องเหล่านี้:

  1. ที่อยู่หรือ address ของ smart contract
  2. ฟังก์ชั่นที่เราต้องการเรียกใช้
  3. ตัวแปรต่างๆ หรือ variable ที่เราต้องการใส่มันลงไปในฟังก์ชั่น

Ethereum node สามารถใช้ภาษา JSON-RPC ได้เท่านั้น ซึ่งเป็นภาษาที่อ่านได้ง่ายมากๆ (human-readable) การ query อกกมา เพื่อบอก node ที่เราต้องการเรียกใช้ฟังก์ชั่นบน contract ก็จะมีหน้าตาประมาณนี้:

// ใช่แล้ว... ขอให้โชคดีกับการเขียนฟังก์ชั่นทั้งหมดด้วยรูปแบบนี้ก็แล้วกัน!
// เลื่อนไปทางขวา ==>
{"jsonrpc":"2.0","method":"eth_sendTransaction","params":[{"from":"0xb60e8dd61c5d32be8058bb8eb970870f07233155","to":"0xd46e8dd67c5d32be8058bb8eb970870f07244567","gas":"0x76c0","gasPrice":"0x9184e72a000","value":"0x9184e72a","data":"0xd46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f072445675"}],"id":1}

โชคดีหน่อยที Web3.js ได้ซ่อนการ query ต่างๆ ด้านล่างหน้าจอแสดงผล หน้าที่ของเราจึงมีเพียงแค่การ interact กับ interface ของ JavaScript ที่สะดวกและรวดเร็วเท่านั้น

แทนที่จะต้องมาสร้าง query ดังข้างต้น เราสามารถเรียกฟัง์ชั่นที่มีหน้าตาประมาณนี้ได้:

CryptoZombies.methods.createRandomZombie("Vitalik Nakamoto 🤔")
  .send({ from: "0xb60e8dd61c5d32be8058bb8eb970870f07233155", gas: "3000000" })

ซึ่งจะขออธิบายเกี่ยวกับ syntax โดยละเอียดในบทถัดๆ ไปในอนาคต แต่ในอันดับแรกเรามาตั้งค่าโปรเจ็กต์ของเราด้วย Web3.js กันก่อนดีกว่า

มาเริ่มกันได้เลย

การสร้าง workflow ภายในโปรเจ็กต์ของเรา จะเห็นได้ว่าสามารถเพิ่ม Web3.js ไปยังโปรเจ็กต์ของเราโดยการใช้เครื่องมือต่างๆ (package tools) ดังนี้:

// ใช้ NPM npm install web3

// ใช้ Yarn yarn add web3

// ใช้ Bower bower install web3

// ...etc.


หรือจริงๆ แล้วแค่ดาวน์โหลดไฟล์ minified `.js` ไฟล์ออกมาจาก<a href="https://github.com/ethereum/web3.js/blob/1.0/dist/web3.min.js" target=_blank>github</a> เท่านั้นก็เพียงพอ แล้วค่อยเพิ่มมันลงไปยังโปรเจ็กต์ของเรา:

<script language="javascript" type="text/javascript" src="web3.min.js"></script>

เนื่องจากเราไม่อยากเดาไปต่างๆ นานาว่า development environment ของเรามีหน้าตาเป็นอย่างนั้นอย่างนี้ รวมถึง package manager ที่ใช้ สำหรับบทเรียนนี้จะมีการเพิ่ม Web3 เข้ามายังโปรเจ็คอย่างง่ายๆ โดยการใช้ script tag ดังข้างบนนั่นเอง

##มาทดสอบกันเลย

หลังจากที่ได้สร้าง shell สำหรับไฟล์โปรเจ็กต์ HTML แล้ว ให้สมมติว่าเรามี copy ของ `web3.min.js` บรรจุอยู่ในโฟลเดอร์เดียวกันกับ `index.html` เป็นที่เรียบร้อย

1. ให้ copy/paste ส่วน script tag ทางด้านบนไปยัง project ของเราเพื่อที่ให้สามารถใข้ `web3.js` ได้