[크립토 좀비] 3일차 첫 커리큘럼 완료

재호·2022년 7월 9일
0

크립토 좀비

목록 보기
3/3

레슨 5

토큰

이더리움에서 토큰 은 기본적으로 그저 몇몇 공통 규약을 따르는 스마트 컨트랙트이다. 그 안에서 누가 얼마나 많은 토큰을 가지고 있는지 기록하고, 몇몇 함수를 가지고 사용자들이 그들의 토큰을 다른 주소로 전송할 수 있게 해주는 것이다.
모든 ERC20 토큰들이 똑같은 이름의 동일한 함수 집합을 공유하기 때문에, 이 토큰들에 똑같은 방식으로 상호작용이 가능하다. 즉 하나의 ERC20 토큰과 상호작용할 수 있는 애플리케이션 하나를 만들면, 이 앱이 다른 어떤 ERC20 토큰과도 상호작용이 가능한 것이다.

본 레슨에서는 ERC20 토큰을 사용하지 않고 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;
}

위는 ERC721 표준의 메소드들이다. 레슨 5에서는 이 메소드들을 전부 오버라이딩할 것이다.

balanceOf ownerOf

balanceOf는 단순히 address를 받아 해당 address가 토큰을 얼마나 가지고 있는지 반환한다.
ownerOf는 토큰 ID를 받아 이를 소유하고 있는 사람의 address를 반환한다.

function balanceOf(address _owner) public view returns (uint256 _balance) {
  return ownerZombieCount[_owner];
}

function ownerOf(uint256 _tokenId) public view returns (address _owner) {
  return zombieToOwner[_tokenId];
}

여기서 주의할 점이 있다. 레슨 4에서 우리는 ownerOf라는 modifier를 만들었다. 동명으로 함구와 제어자를 만들 수 없기 때문에 modifer를 다른 이름으로 수정하도록 하자.

전송 로직

ERC721 스펙에서는 토큰을 전송할 때 2개의 방식이 있다.
1. transfer()함수는 토큰의 소유자가 전송 상대의 address와 전송하고자 하는 _tokenId를 인자로 사용한다.
2. 두 번째 방법은 토큰의 소유자가 먼저 위에서 본 정보들을 가지고 approve()를 호출하고 누가 해당 토큰을 가질 허가를 받았는지 저장한다. 이후 누군가 takeOwnership을 호출하면 이 msg.sender가 허가를 받았는지 확인하고 해당 토큰을 전송해준다.

======transfer=========
function _transfer(address _from, address _to, uint256 _tokenId) private {
   ownerZombieCount[_to]++;
   ownerZombieCount[_from]--;
   zombieToOwner[_tokenId] = _to;
   Transfer(_from, _to, _tokenId);
 }
 
======approve && takeOwnership=========
mapping (uint => address) zombieApprovals;

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);
}
오버플로우 막기

오버플로우란 이진수 8비트의 이진수 11111111에 1을 더하면 이 수는 00000000으로 돌아온다. 이를 오버플로우라 하고, 반대로 00000000에서 1을 빼면 11111111과 같아지는 것을 언더플로우라고 한다.
스마트 컨트랙트를 작성할 때 우리가 인지하고 있어햐 할 주요한 보안 기능중 하나이다.
본 예제에서는 이를 막기 위해 OpenZeppelin에서 만든 SafeMath라는 라이브러리를 사용할 것이다.

라이브러리를 사용할 때는 using SafeMath for uint256이라는 구문을 사용한다. 이 구문을 사용하면 모든 uint256에서 다음과 같은 함수들을 사용할 수 있게 된다.
add, sub, mul, div

using SafeMath for uint256;

uint256 a = 5;
uint256 b = a.add(3); // 5 + 3 = 8
uint256 c = a.mul(2); // 5 * 2 = 10

우리는 레슨을 진행하면서 여러 곳에서 ++연산을 사용했지만 이는 오버플로우를 야기할 수 있기 때문에 ++연산을 모두 SafeMathadd로 수정하여 주면 된다.
ownerZombieZCount[_to]++; => ownerZombieZCount[_to] = ownerZombieZCount[_to].add(1);

레슨 6

Web3.js

이더리움 네트워크는 노드로 구성되어 있고 각 노드는 블록체인의 복사본을 가지고 있다. 내가 스마트 컨트랙트의 함수를 실행하고자 한다면 이이 노드들 중 하나에 질의를 보내 아래 내용을 전달해야 한다.

1. 스마트 컨트랙트 주소
2. 실행하고자 하는 함수
3. 그 함수에 전달하고자 하는 변수들

이더리움 노드들은 JSON-RPC라고 불리는 언어로만 소통할 수 있는데 이는 사람이 읽기는 불편하다. 이를 다행히도 Web3.js가 몰라도 되게 해준다. 그저 자바스크립트 인터페이르소 상호작용을 하면 되는 것이다.

Provider

Web3.js에 Web3 프로바이더를 설정하는 것은 우리 코드에 읽기와 쓰기를 처리하려면 어떤 노드와 통신을 해야 하는지 설정하는 것이다. 우리는 프로바이더로 이더리움 노드를 스스로 운영할 수도 있지만 그런 수고를 덜어주는 서비스가 있다.

Infura

Infura는 빠른 읽기를 위한 캐시 계층을 포함하는 다수의 이더리움 노드를 운영하는 서비스이다. 하지만 많은 사용자들이 DApp을 사용할 것이고 이 사용자들이 단순히 읽기만 하는게 아니기에 우리는 이 사용자들이 그들의 개인 키로 트랜잭션에 서명을 할 수 있도록 해야한다.

Metamask

Metamask는 사용자들이 이더리움 계정과 개인 키를 안전하게 관리할 수 있게 해주는 크롬과 파이어폭스의 브라우저 확장 프로그램이다.

메타마스크는 web3라는 전역 자바스크립트 객체를 통해 브라우저에 Web3 프로바이더를 주입한다. 그러니 우리 앱에서는 web3가 존재하는지 확인하고, 만약 존재한다면 web3.currentProvider를 프로바이더로서 사용하면 된다.

window.addEventListener('load', function() {

  // Web3가 브라우저에 주입되었는지 확인(Mist/MetaMask)
  if (typeof web3 !== 'undefined') {
    // Mist/MetaMask의 프로바이더 사용
    web3js = new Web3(web3.currentProvider);
  } else {
    // 사용자가 Metamask를 설치하지 않은 경우에 대해 처리
    // 사용자들에게 Metamask를 설치하라는 등의 메세지를 보여줄 것
  }

  // 이제 자네 앱을 시작하고 web3에 자유롭게 접근할 수 있네:
  startApp()

})
컨트랙트와 통신하기

메타마스크의 Web3 프로바이러로 Web3.js를 초기화했으니 스마트 컨트랙트와 통신을 할 수 있도록 만들어보자. Web3.js는 스마트 컨트랙트와 통신을 위해 컨트랙트 주소ABI를 필요로 한다.

컨트랙트 주소

스마트 컨트랙트르 모두 작성한 후 컴파일하고 이더리움에 배포를 하게되면 해당 컨트랙트는 영원히 존재하는, 이더리움 상에서 고정된 주소를 얻을 것이다. 이게 컨트랙트 주소이다.

컨트랙트 ABI

ABIApplication Binary Interface 의 줄임말이며 기본적으로 컨트랙트 메소드를 JSON 형태로 표현한 것이다. 컨트랙트가 이해할 수 있도록 하려면 Web3.js가 어떤 형태로 함수 호출을 해야 하는지 알려주느 것이다.

<!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>

    <script>
      var cryptoZombies;

      function startApp() {
        var cryptoZombiesAddress = "YOUR_CONTRACT_ADDRESS";
        cryptoZombies = new web3js.eth.Contract(cryptoZombiesABI, cryptoZombiesAddress);
      }

      function getZombieDetails(id) {
        return cryptoZombies.methods.zombies(id).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에서 컨트랙트 함수를 호출하기 위해서 우리는 callsend를 사용한다.

call은 view와 pure 함수를 위해 사용하기에 로컬 노드에서만 실행하고 블록체인에 트랜잭션을 만들지 않는다.

send는 트랜잭션을 생성하고 블록체인 상의 데이터를 변경시킨다. view와 pure가 아닌 모든 함수에 대해 send를 사용해야 하는 것이다.

myContract.methods.myMethod(123).call()
myContract.methods.myMethod(123).send()
함수들을 호출하며 Web3.js에 구현하기
<!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}</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) {
        // 시간이 꽤 걸릴 수 있으니, 트랜잭션이 보내졌다는 것을
        // 유저가 알 수 있도록 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에서는 컨트랙트의 이벤트를 구독하여 해당 이벤트가 발생할 때마다 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);
indexed 사용하기

이벤트를 필터링하고 현재 사용자와 연관된 변경만을 수신하기 위해 솔리디티 컨트랙트에 indexed 키워드를 사용해야 한다.

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

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

getPastEvents를 이용해 지난 이벤트들에 대해 질의를 하고, fromBlocktoBlock 필터들을 이용해 로그에 대한 시간 범위를 솔리디티에 전달할 수 있다.

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

위 메소드를 사용해서 시작 시간부터의 이벤트 로그들에 대해 질의를 할 수 있기 때문에, 이벤트를 저렴한 형태의 storage로 사용할 수 있다.

여기서 단점이 되는 부분은 스마트 컨트랙트 자체 안에서는 이벤트를 읽을 수 없다는 것이다. 하지만 히스토리를 블록체인에 기록하여 앱의 프론트엔드에서 읽기를 원하는 데이터가 있다면, 이는 꽤 유리할 것이다.

모두 적용한 Web3.js 코드
<!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];
           
            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) {
         
          getZombieDetails(id)
          .then(function(zombie) {
           
           
            $("#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) {
       
       
        $("#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 + "!");
         
          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() {
       
        if (typeof web3 !== 'undefined') {
         
          web3js = new Web3(web3.currentProvider);
        } else {
         
        }
        startApp()
      })
    </script>
  </body>
</html>

코스 완료

레슨 6을 끝으로 첫 코스가 마무리되었다. 다음으로 코스가 아직 여러개 더 있지만 한글로 번역이 되어있지 않기 때문에 진행에 많은 어려움이 있을 것 같아서 첫 코스에서 배운 내용을 기반으로 다른 방법을 찾아 공부를 해야 할 것 같다.

profile
Java, Spring, SpringMVC, JPA, MyBatis

0개의 댓글