Skip to content

Latest commit

 

History

History
678 lines (563 loc) · 27.2 KB

09.md

File metadata and controls

678 lines (563 loc) · 27.2 KB
title actions requireLogin material
이벤트(Event) 구독하기
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) { // 우리 컨트랙트에서 좀비 상세 정보를 찾아, `zombie` 객체 반환 getZombieDetails(id) .then(function(zombie) { // HTML에 변수를 넣기 위해 ES6의 "template literal" 사용 // 각각을 #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를 업데이트해야 함 $("#txStatus").text("Creating new zombie on the blockchain. This may take a while..."); // 우리 컨트랙트에 전송하기: return CryptoZombies.methods.createRandomZombie(name) .send({ from: userAccount }) .on("receipt", function(receipt) { $("#txStatus").text("Successfully created " + name + "!"); // 블록체인에 트랜잭션이 반영되었으며, UI를 다시 그려야 함 getZombiesByOwner(userAccount).then(displayZombies); }) .on("error", function(error) { // 사용자들에게 트랜잭션이 실패했음을 알려주기 위한 처리 $("#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("좀비를 레벨업하는 중..."); return CryptoZombies.methods.levelUp(zombieId) .send({ from: userAccount, value: web3.utils.toWei("0.001") }) .on("receipt", function(receipt) { $("#txStatus").text("압도적인 힘! 좀비가 성공적으로 레벨업했습니다."); }) .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() { // Web3가 브라우저에 주입되었는지 확인(Mist/MetaMask) if (typeof web3 !== 'undefined') { // Mist/MetaMask의 프로바이더 사용 web3js = new Web3(web3.currentProvider); } else { // 사용자가 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 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; } }
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; } }
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); cryptoZombies.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) { // 우리 컨트랙트에서 좀비 상세 정보를 찾아, `zombie` 객체 반환 getZombieDetails(id) .then(function(zombie) { // HTML에 변수를 넣기 위해 ES6의 "template literal" 사용 // 각각을 #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를 업데이트해야 함 $("#txStatus").text("Creating new zombie on the blockchain. This may take a while..."); // 우리 컨트랙트에 전송하기: return CryptoZombies.methods.createRandomZombie(name) .send({ from: userAccount }) .on("receipt", function(receipt) { $("#txStatus").text("Successfully created " + name + "!"); // 블록체인에 트랜잭션이 반영되었으며, UI를 다시 그려야 함 getZombiesByOwner(userAccount).then(displayZombies); }) .on("error", function(error) { // 사용자들에게 트랜잭션이 실패했음을 알려주기 위한 처리 $("#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("좀비를 레벨업하는 중..."); return CryptoZombies.methods.levelUp(zombieId) .send({ from: userAccount, value: web3.utils.toWei("0.001") }) .on("receipt", function(receipt) { $("#txStatus").text("압도적인 힘! 좀비가 성공적으로 레벨업했습니다."); }) .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() { // Web3가 브라우저에 주입되었는지 확인(Mist/MetaMask) if (typeof web3 !== 'undefined') { // Mist/MetaMask의 프로바이더 사용 web3js = new Web3(web3.currentProvider); } else { // 사용자가 Metamask를 설치하지 않은 경우에 대해 처리 // 사용자들에게 Metamask를 설치하라는 등의 메세지를 보여줄 것 } // 이제 자네 앱을 시작하고 web3에 자유롭게 접근할 수 있네: startApp() }) </script> </body> </html>

자네도 볼 수 있다시피, Web3.js를 통해 컨트랙트와 상호작용 하는 것은 꽤 간단하네 - 한번 자네의 환경을 구축하고 나면, 함수를 호출하고 트랜잭션을 전송하는 것은 일반적인 웹 API와 크게 다르지 않네.

여기 우리가 다루고자 하는 것이 하나 더 있네 - 자네의 컨트랙트에서 이벤트를 구독하는 것이네.

새로운 좀비 수신하기

zombiefactory.sol을 다시 생각해보면, 새로운 좀비가 생성될 때마다 매번 호출되던 NewZombie라는 이벤트가 있었네:

event NewZombie(uint zombieId, string name, uint dna);

Web3.js에서 자네는 이벤트를 구독하여 해당 이벤트가 발생할 때마다 Web3 프로바이더가 자네의 코드 내의 어떠한 로직을 실행시키도록 할 수 있네:

cryptoZombies.events.NewZombie()
.on("data", function(event) {
  let zombie = event.returnValues;
  // `event.returnValue` 객체에서 이 이벤트의 세 가지 반환 값에 접근할 수 있네:
  console.log("새로운 좀비가 태어났습니다!", zombie.zombieId, zombie.name, zombie.dna);
}).on("error", console.error);

이건 DApp에서 어떤 좀비가 생성되든지 항상 알림을 보낼 거라는 걸 명심하게 - 현재 사용자의 좀비만이 아니라는 것이네. 현재 사용자가 만든 것에 대해서만 알림을 보내고 싶다면 어떻게 해야 하겠는가?

indexed 사용하기

이벤트를 필터링하고 현재 사용자와 연관된 변경만을 수신하기 위해, 우리의 ERC721을 구현할 때 Transfer 이벤트에서 했던 것처럼 우리의 솔리디티 컨트랙트에 indexed 키워드를 사용해야 하네.

event Transfer(address indexed _from, address indexed _to, uint256 _tokenId);

이 경우, _from_toindexed 되어 있기 때문에, 우리 프론트엔드의 이벤트 리스너에서 이들을 필터링할 수 있네:

// `filter`를 사용해 `_to`가 `userAccount`와 같을 때만 코드를 실행
cryptoZombies.events.Transfer({ filter: { _to: userAccount } })
.on("data", function(event) {
  let data = event.returnValues;
  // 현재 사용자가 방금 좀비를 받았네!
  // 해당 좀비를 보여줄 수 있도록 UI를 업데이트할 수 있도록 여기에 추가
}).on("error", console.error);

자네도 볼 수 있듯이, eventindexed 영역을 사용하는 것은 자네 컨트랙트에서 변화를 감지하고 프론트엔드에 반영할 수 있게 하는 유용한 방법이네.

지난 이벤트에 대해 질의하기

우린 getPastEvents를 이용해 지난 이벤트들에 대해 질의를 하고, fromBlocktoBlock 필터들을 이용해 이벤트 로그에 대한 시간 범위를 솔리디티에 전달할 수 있네(여기서 "block"은 이더리움 블록 번호를 나타낸다네).

cryptoZombies.getPastEvents("NewZombie", { fromBlock: 0, toBlock: "latest" })
.then(function(events) {
  // `events`는 우리가 위에서 했던 것처럼 반복 접근할 `event` 객체들의 배열이네.
  // 이 코드는 생성된 모든 좀비의 목록을 우리가 받을 수 있게 할 것이네.
});

위 메소드를 사용해서 시작 시간부터의 이벤트 로그들에 대해 질의를 할 수 있기 때문에, 이를 통해 흥미로운 사용 예시를 만들 수 있네: 이벤트를 저렴한 형태의 storage로 사용하는 것이네.

다시 생각해보면, 데이터를 블록체인에 기록하는 것은 솔리디티에서 가장 비싼 비용을 지불하는 작업 중 하나였네. 하지만 이벤트를 이용하는 것은 가스 측면에서 훨씬 더 저렴하네.

여기서 단점이 되는 부분은 스마트 컨트랙트 자체 안에서는 이벤트를 읽을 수 없다는 것이네. 하지만 히스토리로 블록체인에 기록하여 앱의 프론트엔드에서 읽기를 원하는 데이터가 있다면, 이는 새겨놓아야 할 중요한 사용 예시이네.

예를 들어, 우린 이것을 좀비 전투의 히스토리 기록용으로 사용할 수 있네 - 좀비가 다른 좀비를 공격할 때마다, 그리고 누군가 이길 때마다 우린 이벤트를 생성할 수 있네. 스마트 컨트랙트는 추후 결과를 계산할 때 이 데이터가 필요하지 않지만, 사용자들이 앱의 프론트엔드에서 찾아볼 수 있는 유용한 데이터이지.

직접 해보기

Transfer 이벤트를 감지할 수 있는 코드를 추가해보도록 하지. 그리고 현재 사용자가 새로운 좀비를 받았을 떄 우리 앱의 UI를 업데이트하도록 하겠네.

우린 이 코드를 startApp 코드의 끝부분에 추가해야 하네. 이벤트 리스너를 추가하기 전에 cryptoZombies 컨트랙트가 확실히 초기화될 수 있도록 하기 위해서이지.

  1. startApp()의 끝부분에 crpytoZombies.events.Transfer를 수신하는 위의 코드 블록을 복사/붙여넣기 하게.

  2. UI를 업데이트 해야 하는 부분에 getZombiesByOwner(userAccount).then(displayZombies);를 사용하게.