
이더리움 노드는 JSON-RPC 언어로 서로 소통
{"jsonrpc":"2.0","method":"eth_sendTransaction","params":[{"from":"0xb60e8dd61c5d32be8058bb8eb970870f07233155","to":"0xd46e8dd67c5d32be8058bb8eb970870f07244567","gas":"0x76c0","gasPrice":"0x9184e72a000","value":"0x9184e72a","data":"0xd46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f072445675"}],"id":1}
만약 스마트 컨트랙트 함수를 실행하고 싶다면, 노드를 선택하고
Web3.js를 이용하면 편리하고 쉽게 읽을 수 있는 JS 인터페이스로 상호작용 가능
CryptoZombies.methods.createRandomZombie("Vitalik Nakamoto 🤔")
.send({ from: "0xb60e8dd61c5d32be8058bb8eb970870f07233155", gas: "3000000" })
<script language="javascript" type="text/javascript" src="web3.min.js"></script>
<!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>
<!-- 여기에 web3.js를 포함하게. -->
<script language="javascript" type="text/javascript" src="web3.min.js"></script>
</head>
<body>
</body>
</html>
어떤 노드와 통신할지 설정해야 되니까
빠른 읽기를 위한 캐시 계층을 포함하는 다수 이더리움 노드 운영 서비스
접근을 위한 API를 무료로 사용 가능
var web3 = new Web3(new Web3.providers.WebsocketProvider("wss://mainnet.infura.io/ws"));
단순히 '읽기'(접근)만 하는 것이 아닌 '쓰기'도 할 것이기에,
사용자들이 개인키로 서명을 할 수 있게 해야겠지
앱 프론트엔드에서 개인키를 관리? 되겠냐?
이를 처리해주는 서비스가 있지
이더리움 계정과 개인키를 안전하게 관리해주는 브라우저 확장 프로그램
이 계정을 써서 Web3.js를 이용하는 웹사이트들과 상호작용할 수 있도록 한다.
사용자가 웹 브라우저를 써서 웹사이트를 통해 Dapp과 상호작용한다면, 필요!
(메타마스크는 내부적으로 Infura 서버를 Web3 provider로 사용, 하지만 그들만의 provider를 선택할 수 있는 옵션도 있음)
web3 라는 전역 자바스크립트 객체를 통해 브라우저에 프로바이더를 주입
즉, 앱에서 web3가 있는지 확인만 하고 있다면 web3.currentProvider를 사용하면 됨
window.addEventListener('load', function() {
// Web3가 브라우저에 주입되었는지 확인
if(typeof web3 !== 'undefined') {
//Mist/MetaMask 프로바이더 사용
web3js = new Web3(web3.currentProvider);
} else {
//사용자가 Metamask를 설치하지 않은 경우에 처리
// 사용자에게 설치하라는 메세지 보여줄 것
}
//이제 앱 실행하고 web3에 자유롭게 접근가능
startApp()
})
Web3.js는 스마트컨트랙트와 통신 위해 2가지 필요
기본적으로 JSON 형태로 컨트랙트의 메서드를 표현하는 것
컨트랙트가 이해할 수 있도록 Web3.js가 어떤 형태로 함수호출을 해야 하는지 알려주는 것
// myContract 인스턴스화
var myContract = new web3js.eth.Contract(myABI, myContractAddress);
// 2. 여기서 코딩을 시작하게
var cryptoZombies;
function startApp() {
var CryptoZombiesAddress = "YOUR_CONTRACT_ADDRESS";
cryptoZombies = web3js.eth.Contract(cryptozombiesABI, cryptoZombiesAddress);
}
web3.js는 컨트랙트 함수 호출하기위해 call / send 메서드 사용
view, pure 함수 위해 사용, 트랜잭션 만들지 않음
가스도 안 쓰고, 서명할 필요도 없음
myContract.methods.myMethod(123).call()
트랜잭션을 만들고 블록체인 상의 데이터를 변경
view, pure가 아닌 모든 함수에 대해 send 사용해야
가스 지불, 메타마스크에서 서명하라고 할 거임
(메타마스크로 하니까 다 알아서 해주고 ㅈㄴ 편하네?)
myContract.methods.myMethod(123).send()
솔리디티에서 public으로 변수를 선언하면 자동으로
같은 이름의 "getter"함수를 만들어 놓는다고 했었다.
Zombie[] public zombies;
라고 한다면
zombies(id)로 찾을 수 있다는 뜻이다
function getZombieDetails(id) {
return cryptoZombies.methods.zombies(id).call()
}
// 함수를 호출하고 결과를 가지고 무언가를 처리:
getZombieDetails(15)
.then(function(result) {
console.log("Zombie 15: " + JSON.stringify(result));
});
비동기적 Promise를 반환
result는 이런식
{
"name": "H4XF13LD MORRIS'S COOLER OLDER BROTHER",
"dna": "1337133713371337",
"level": "9999",
"readyTime": "1522498671",
"winCount": "999999999",
"lossCount": "0" // Obviously.
}
function getZombieDetails(id) {
return cryptoZombies.methods.zombies(id).call()
}
function zombieToOwner(id) {
return cryptoZombies.methods.zombieToOwner(id).call()
}
function getZombiesByOwner(owner) {
return cryptoZombies.methods.getZombies.ByOwner(owner).call()
}
만약 홈페이지에 사용자의 전체 좀비 군대를 보여주고 싶다면?
getZombiesByOwner(owner)를 사용하면 될텐데,
여기서 owner는 솔리디티 주소일텐데, 어떻게 사용자의 주소를 알아낼까?
메타마스크에서는 주입된 web3 변수 안에 '현재 활성화된 계정'이 뭔지
var userAccount = web3.eth.accounts[0]
로 알아낼 수 있음
사용자가 메타마스크에서 활성화된 계정을 바꿀 수 있기 때문에, 계속 감시하고 업데이트를 해줘야 한다.
그래서 setInterval로 0.1초마다 현재 활성화 계정을 확인하고, 다르다면 업데이트 하는 식으로 할 수 있겠지.
var accountInterval = setInterval(function() {
// 계정 바뀌었는지 확인
if (web3.eth.accounts[0] !== userAccount) {
userAccount = web3.eth.accounts[0];
//새 계정에 대한 UI로 업데이트 위한 함수호출
updateInterface();
}
},100);
var userAccount;
var accountInterval = setInterval(function() {
if (web3.eth.accounts[0] !== userAccount) {
userAccount = web3.eth.accounts[0];
getZombiesByOwner(userAccount)
.then(displayZombies);
}
}, 100);
리액트, 뷰 : 여기서 다루지 않음
jQuery로 데이터를 파싱하고 표현하자
이미
<div id="zombies"></div>
존재
getZombies
받은 DNA 문자열을 이미지로 어떻게 바꿀까?
DNA 문자열을 부분 문자열로 나누고, 모든 2자리 숫자를 이미지에 대응시켜 작업을 했었다.
// 좀비 머리 표현하는 1-7 정수 얻기
var head = parseInt(zombie.dna.substring(0,2)) % 7 + 1
// 순차적인 파일 이름으로 7개의 머리 이미지
var headSrc = "../assets/zombieparts/head-" + head + ".png"
function displayZombies(ids) {
$("#zombies").empty();
for (id of ids) {
getZombieDetail(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>`);
});
}
}
호출한 사람 주소 필요, 서명하라고 할거고, 가스 소모
트랜잭션 전송하고 적용될 때 까지 시간이 오래걸릴 거임
Web3.js에서 호출하기
function createRandomZombie(name) {
// 시간이 걸리니, 유저가 알 수 있도록 UI 업뎃
$("#txStatus").text("Creating new Zombie on the block chain. 기다려줘");
// 컨트랙트에 전송
return CryptoZombies.methods.createRandomZombie(name)
.send({from:userAccount})
.on("receipt", function(receipt) {
$("#txStatus").text("성공적으로" + name + "생성 했습니다!");
// UI 다시 그려야지
getZombiesByOwner(userAccount).then(displayZombies);
.on("error", function(error) {
//실패했음을 알려주기
$("#txStatus").text(error);
});
}
receipt는 트랜잭션이 이더리움 블록에 포함될 때 발생
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);
});
}
레벨업 함수에서 payable 함수를 사용했었지
function levelUp(uint _zombieId) external payable {
require(msg.value == levelUpFee);
zombies[_zombieId].level++;
}
wei는 이더의 가장 작은 단위
( 1 eth = 10^18 wei)
// 1 이더를 웨이로 변환
web3js.utils.toWei("1");
CryptoZombies.methods.levelUp(zombieId)
.send({ from: userAccount, value: web3js.utils.toWei("0.001") })
이렇게 레벨업 함수 쓸 때 사용자가 0.001 이더 보내게 할 수 있음
function levelUp(zombieId) {
$("#txStatus").text("좀비를 레벨업하는 중...");
return CryptoZombies.methods.levelUp(zombieId)
.send({ from: userAccount, value: web3js.utils.toWei("0.001") })
.on("receipt", function(receipt) {
$("#txStatus").text("압도적인 힘! 좀비가 성공적으로 레벨업했습니다.");
})
.on("error", function(error) {
$("#txStatus").text(error);
});
}
event NewZombie(uint zombieId, string name, uint dna);
web3.js에서 이벤트를 구독하여 해당 이벤트가 발생할 때 마다 프로바이더가 코드 내의 어떠한 로직을 실행시킬 수 있도록 가능
cryptoZombies.events.NewZombie()
.on("data", function(event) {
let zombie = event.returnValues;
// 'event.returnValues' 객체에서 이 이벤트의 세 가지 반환 값에 접근 가능
console.log("새로운 좀비 생성!", zombie.zombieId, zombie.name, zombie.dna);
}).on("error", console.error);
이러면 모든 좀비가 태어날 때 마다 모든 사용자에게 알림이 가겠지, "현재 사용자"가 만든 것에 대한 알람만 보내고 싶은데?
이벤트 필터링하고, 현재 사용자와 연관된 변경만을 수신하기 위해, Transfer 이벤트처럼 indexed를 사용해야됨
event Transfer(address indexed _from, address indexed _to, uint256 _tokenId);
_from, _to가 indexed되어 있기 때문에, 프론트의 이벤트리스터에서 필터링 가능
// filiter로 _to가 userAccount와 같을 때만 코드 실행
cryptoZombies.events.Transfer({ filter: {_to: userAccount})
.on("data", function(event) {
let data = event.returnValues;
// 여기서 UI 업데이트
}).on("error", console.error);
getPastEvents를 이요해 지난 이벤트들에 질의하고, fromBlock toBlock 필터를 이용해 이벤트 로그에 대한 시간 범위를 이용 가능
cryptoZombies.getPastEvents("NewZombie", { fromBlock: 0, toBlock: "latest" })
.then(function(events) {
// `events`는 우리가 위에서 했던 것처럼 반복 접근할 `event` 객체들의 배열이네.
// 이 코드는 생성된 모든 좀비의 목록을 우리가 받을 수 있게 할 것이네.
});
대신 스마트 컨트랙트 자체 안에서는 이벤트를 읽을 수 없다.
히스토리로 블록체인에 기록하여 앱의 프론트에서 읽기를 원하는 데이터가 있다면, 아주 중요한 사용 예시
좀비 공격, 누가 이길 떄 마다 이벤트를 생성한다면,
스마트 컨트랙트는 추후 결과를 계산할 때 이 데이터가 필요하지 않지만, 사용자들이 앱의 프론트엔드에서 찾아볼 수 있는 유용한 데이터
cryptoZombies.events.Transfer({ filter: { _to: userAccount } })
.on("data", function(event) {
let data = event.returnValues;
getZombiesByOwner(userAccount).then(displayZombies);
}).on("error", console.error);