KoreanFoodie's Study

Solidity 튜토리얼 #6 : Web3.js 와 이더리움 연동하기 본문

Tutorials/Solidity

Solidity 튜토리얼 #6 : Web3.js 와 이더리움 연동하기

GoldGiver 2022. 9. 23. 15:07

크립토 좀비에서 제공하는 튜토리얼을 통해 배우는 Solidity 문법을 정리하고 있습니다!

Lesseon 6 에서는 Web3.js 를 이용해 이더리움과 front-end 사이의 동작에 대해 배울 수 있었다.

index.html 이라는 코드에 필요한 자바스크립트 코드를 정리해 놓았다.

 

 

이번 장을 정리한 내용은 다음과 같다.

 

1. Web3.js

smart contract 상에서 함수를 호출하고 싶으면, 노드에게 다음과 같은 내용을 전달해야 한다.

  • smart contract 의 주소
  • 호출할 함수와 전달할 매개변수

이때, 이더리움 노드는 JSON-RPC 라는 언어로 소통한다. JSON-RPC 쿼리문은 다음과 같은 형식으로 이루어져 있다.

// Yeah... Good luck writing all your function calls this way!
// Scroll right ==>
{"jsonrpc":"2.0","method":"eth_sendTransaction","params":[{"from":"0xb60e8dd61c5d32be8058bb8eb970870f07233155","to":"0xd46e8dd67c5d32be8058bb8eb970870f07244567","gas":"0x76c0","gasPrice":"0x9184e72a000","value":"0x9184e72a","data":"0xd46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f072445675"}],"id":1}

의와 같은 방식으로 코드를 짜는 건 매우 힘든 일일 것이다. 대신, 자바스크립트로 위와 같은 쿼리문을 작성할 수 있다.

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

 

일단 우리가 사용하는 html 파일에 다음 줄을 추가하자.

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

 

 

2. Infura : Web3 Providers

원래는 함수를 호출할 때 함수를 실행할 노드의 주소를 지정해야 하지만, 매번 해당 작업을 하는 건 매우 번거롭고 귀찮은 일이다. 대신, API 호출을 통해 무료로 일부 노드들에 접근할 수 있게 해주는 서비스가 있는데, 그게 Infura 이다.

Infura 를 이용하면 노드를 직접 구축할 필요없이 캐싱된 이더리움 노드들을 활용할 수 있다. 다음과 같이 Web3 Provider 를 Infura 로 지정한다.

var web3 = new Web3(new Web3.providers.WebsocketProvider("wss://mainnet.infura.io/ws"));

그런데 위와 같은 방식은 사용자의 private 키를 필요로 하는 단점이 있다. 이는 매우 위험한 행위가 될 수 있으므로, 예제에서는 Metamask 를 Web3 Provider 로 지정했다. 아래 코드는 브라우저에 이미 깔려 있는 provider (Metamask 나 Mint) 를 detect 하는 함수이다.

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 web3. Probably
    // show them a message telling them to install Metamask in
    // order to use our app.
  }

  // Now you can start your app & access web3js freely:
  startApp()

})

 

 

3. Talking to Contracts

Web3.js 는 contract 와 소통하기 위해 두 가지가 필요하다 : address, ABI

contract 의 address 와 ABI 가 있으면, 다음과 같이 Web3 에서 contract 를 Instantiate 할 수 있다.

var myContract = new web3js.eth.Contract(myABI, myContractAddress);

 

 

4. Calling Contract Functions

Web3.js 에는 contract 의 함수를 호출 할 수 있는 두 종류의 메서드가 있다 : call, send

 

call 은 view 와 pure 함수들에 사용되며, local node 에서만 돌아간다. 따라서 가스비가 들지 않는다. contract 에 있는 myMethod 라는 함수를 호출하고 싶으면 다음과 같이 하면 된다.

myContract.methods.myMethod(123).call()

 

send 는 블록체인의 데이터를 변경한다. send 는 view 나 pure 가 아닌 함수를 호출하는데 사용된다.

myContract.methods.myMethod(123).send()

사실 send 함수는 가스를 소모한다. 그래서 소모되는 양을 전달해야 하지만, 메타마스크가 Web3 Provider 로 잡혀 있는 경우, 이 과정을 알아서 처리해 준다!

이전 게시글에서 정의한 zombie 구조체의 정보를 불러오는 함수를 보자.

function getZombieDetails(id) {
  return cryptoZombies.methods.zombies(id).call()
}

// Call the function and do something with the result:
getZombieDetails(15)
.then(function(result) {
  console.log("Zombie 15: " + JSON.stringify(result));
});

...

/// result
{
  "name": "H4XF13LD MORRIS'S COOLER OLDER BROTHER",
  "dna": "1337133713371337",
  "level": "9999",
  "readyTime": "1522498671",
  "winCount": "999999999",
  "lossCount": "0" // Obviously.
}

 

 

5. MetaMask 에서 user account 가져오기

메타마스크에서 계정을 가져와 보자.

// 현재 active 한 계정 찾기
var userAccount = web3.eth.accounts[0]

// 100 밀리초마다 userAccount 가 현재 활성화된 계정인지 검사
var accountInterval = setInterval(function() {
  // Check if account has changed
  if (web3.eth.accounts[0] !== userAccount) {
    userAccount = web3.eth.accounts[0];
    // Call some function to update the UI with the new account
    updateInterface();
  }
}, 100);

 

 

6. contract 로부터 zombie 정보 불러오기

아래 코드는 우리가 생성한 zombie 의 정보를 조회하는 코드이다.

// 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);
  });
}

위에서 receipt 는 transaction 이 이더리움의 블록 안에 들어 갔을 때 fire 된다(즉, 좀비가 생성되고 contract 에 저장되면). error 는 문제가 있어 transaction 이 실패했을 때 fire 된다.

 

 

7. payable function 호출

다음 함수는 가스를 지불해서 좀비를 레벨업 시키는 함수이다.

function levelUp(zombieId) {
  $("#txStatus").text("Leveling up your zombie...");
  return cryptoZombies.methods.levelUp(zombieId)
  .send({ from: userAccount, value: web3js.utils.toWei("0.001", "ether") })
  .on("receipt", function(receipt) {
    $("#txStatus").text("Power overwhelming! Zombie successfully leveled up");
  })
  .on("error", function(error) {
    $("#txStatus").text(error);
  });
}

wei 는 Ether 의 sub-unit 으로, 가스비 지불에 사용된다. 10^18 wei 가 1 ether 로, web3js.utils.toWei("1") 같은 구문으로 1 ETH 를 wei 로 바꿔줄 수 있다.

 

 

8. Subscribing to Events

Web3.js 에서는 다음과 같이 이벤트를 구독할 수 있다.

// 이벤트
event NewZombie(uint zombieId, string name, uint dna);

// 이벤트 구독
cryptoZombies.events.NewZombie()
.on("data", function(event) {
  let zombie = event.returnValues;
  // We can access this event's 3 return values on the `event.returnValues` object:
  console.log("A new zombie was born!", zombie.zombieId, zombie.name, zombie.dna);
}).on("error", console.error);

 

indexed 를 사용하면 이벤트를 호출할 때 우리가 원하는 케이스를 분류할 수 있다. 예를 들어, 이벤트를 호출할 때 현재 유저와 관련이 있을 경우에만 함수를 실행시킨다고 가정해 보자. 그 경우, 다음과 같이 코드를 짜면 된다.

// Transfer 이벤트
event Transfer(address indexed _from, address indexed _to, uint256 _tokenId);

// Use `filter` to only fire this code when `_to` equals `userAccount`
cryptoZombies.events.Transfer({ filter: { _to: userAccount } })
.on("data", function(event) {
  let data = event.returnValues;
  // The current user just received a zombie!
  // Do something here to update the UI to show it
}).on("error", console.error);


// 과거의 이벤트도 query 가 가능하다
cryptoZombies.getPastEvents("NewZombie", { fromBlock: 0, toBlock: "latest" })
.then(function(events) {
  // `events` is an array of `event` objects that we can iterate, like we did above
  // This code will get us a list of every zombie that was ever created
});

과거 정보를 조회할때, 이벤트를 사용하는 것이 싸게 먹힐 수 있다!

 

 

이제 완성된 코드를 보면서 Wrap-Up 을 하자!

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;

      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("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() {

       
        if (typeof web3 !== 'undefined') {
         
          web3js = new Web3(web3.currentProvider);
        } else {
         
         
        }

       
        startApp()

      })
    </script>
  </body>
</html>
Comments