KoreanFoodie's Study
Solidity 튜토리얼 #6 : Web3.js 와 이더리움 연동하기 본문
크립토 좀비에서 제공하는 튜토리얼을 통해 배우는 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>
'Tutorials > Solidity' 카테고리의 다른 글
Solidity 튜토리얼 #5 : ERC721 과 SafeMath (0) | 2022.09.22 |
---|---|
Solidity 튜토리얼 #4 : 이더리움 전송 및 payable 함수 (0) | 2022.09.22 |
Solidity 튜토리얼 #3 : 심화 문법 (0) | 2022.09.21 |
Solidity 튜토리얼 #2 : 기본 문법 추가 (0) | 2022.09.21 |
Solidity 튜토리얼 #1 : 기본 문법 (0) | 2022.09.20 |