Lesson4에서는 새로운 개념들 보다는 기존의 개념들을 복습하고 코드 리팩토링을 하였다. 핵심 개념들을 복습하여보자.
modifier onlyOwner() {
require(msg.sender == owner);
_;
}
위의 modifier는 msg.sender == owner여야 한다는 제약을 준다.
이 modifier에 의해 작성된 함수는 modifier를 거친 뒤, _;부터 실행한다.
솔리디티의 함수는 private, internal, public, exteral중 하나로 명시되어야 한다.
- private : 해당 컨트랙트에서만 호출될 수 있고, 상속되는 컨트랙트나 다른 외부 컨트랙트에서 호출될 수 없다. 또한 함수를 정의할때 이름 앞에 _ 를 붙이는 관례가 있다.
- internal : private와 비슷한 성격이다. 컨트랙트 내부와 상속된 컨트랙트에서 호출될 수 있다. private와 마찬가지로 함수이름 앞에 언더바를 붙여준다.
- public : 해당 컨트랙트 뿐만 아니라, 외부 컨트랙트에서도 호출될 수 있다.
- external : 컨트랙트 외부에서만 호출될 수 있다. 다른 컨트랙트와 트랜잭션으로만 호출될 수 있다.
함수의 상태를 제어하는 제어자에는 view와 pure가 있다. 컨트랙트 외부에서 불렀을때 가스를 소모하지 않는다는 특징이 있다.
- view : view는 읽기 전용으로, 블록체인 네트워크 상의 데이터를 읽기만 하고 수정할 수 없습니다.
function getKittyName() view {
return addressToKitty[msg.sender];
}
pure : pure는 순수함수로, 오로지 함수의 파라미터 값을 이용해 return을 하는 함수입니다. 블록체인 네트워크 상의 어떠한 데이터도 읽지 않는 역할을 합니다.
function Calc (uint a, uint b, uint c) pure {
return (a * b + c) * (c - a) + b;
}
컨트랙트에 예금하는 함수를 다음과 같이 payable 제어자를 이용해 작성할 수 있다.
function deposit() payable {
deposits[msg.sender] += msg.value;
};
contract OnlineStore {
function buySomething() external payable {
require(msg.value == 0.001 ether);
transferThing(msg.sender);
}
}
function withdraw() external onlyOwner {
owner.transfer(this.balance);
}
uint random = uint(keccak256(now, msg.sender, randNonce)) % 100;
now, msg.sender, randNonce를 통해서 해시값으로 만든 뒤, uint형으로 바꾸고 마지막 2자리만 받아와서 0부터 99까지의 숫자를 얻을 수 있다.
[출처] : (https://hackernoon.com/ethereums-erc-20-tokens-explained-simply-88f5f8a7ae90)
ERC20
이더리움 네트워크 상에서 유통할 수 있는 토큰의 호환성을 보장하기 위한 표준 사양중 하나이다.
이더리움을 안드로이드나 ios같은 플랫폼으로 생각하면, ERC20은 그 위에서 구동되는 앱이라고 비유할 수 있다.
ERC 721에서 제공하는 인터페이스이다. 메서드를 구현하여 ERC721 토큰을 생성할 수 있다.
[출처]: (https://docs.openzeppelin.com/contracts/3.x/api/token/erc721)
메서드를 작성하기 위하여, ERC721.sol 파일을 통해 ERC721 컨트랙트를 상속하고 메서드를 구현하여 준다.
contract ZombieOwnership is ZombieAttack, ERC721 {
}
위의 코드와 같이 다중 상속을 지원하기 때문에, 두 컨트랙트를 상속하여 온다.가장 오른쪽의 컨트랙트의 메서드부터 오버라이딩을 받기 때문에 ERC721을 오른쪽에 적어준다.
function balanceOf(address _owner) public view returns (uint256 _balance) {
return ownerZombieCount[_owner];
}
function ownerOf(uint256 _tokenId) public view returns (address _owner) {
return zombieToOwner[_tokenId];
}
modifier onlyOwnerOf(uint _zombieId) {
require(msg.sender == zombieToOwner[_zombieId]);
_;
}
1. transfer
전송상대의 addess와 보내고자 하는 token의 Id와 함께 함수를 실행한다. 토큰의 소유자가 전송하는 방식이다.
2. approve, takeOwnership
토큰의 소유자가 자신의 토큰을 가져갈 수 허가를 받은 주소를 매핑한다. 이후 허가를 받은 사람이 takeOwnership을 통해 토큰을 가져갈 수 있다.
_transfer함수를 만들어 1번과 2번 공통의 전송로직을 작성한다.
function _transfer(address _from, address _to, uint256 _tokenId) private {
ownerZombieCount[_to]++; // 토큰을 받는 사람의 count++
ownerZombieCount[_from]--; // 토큰을 주는 사람의 count--
zombieToOwner[_tokenId] = _to; // 토큰의 주인 바꾸기
Transfer(_from, _to, _tokenId); // 전송 이벤트 실행
}
토큰 전송함수에 전송로직을 작성하였다. onlyOwnerOf 제어자를 통해 토큰의 주인만 보낼수 있다.
function transfer(address _to, uint256 _tokenId) public onlyOwnerOf(_tokenId) {
_transfer(msg.sender, _to, _tokenId);
}
zombieApprovals매핑을 통해 토큰의 아이디를 통해 허가받은 주소를 확인한다. approval함수를 실행하고 마지막에는 ERC721의 Approval이벤트를 실행한다.
mapping (uint => address) zombieApprovals;
function approve(address _to, uint256 _tokenId) public onlyOwnerOf(_tokenId) {
zombieApprovals[_tokenId] = _to;
Approval(msg.sender, _to, _tokenId);
}
토큰을 받는사람이 토큰을 가져가도록 하는 함수이다. require를 통해 받는 사람이 가져갈 자격이 있는지 확인한다.
function takeOwnership(uint256 _tokenId) public {
require(zombieApprovals[_tokenId] == msg.sender);
address owner = ownerOf(_tokenId);
_transfer(owner, msg.sender, _tokenId);
}
uint256를 사용한다고 하더라도, 2^256가지나 되지만 혹시나 만일의 overflow라는 상황에 대비해 보호장치를 해주어야 한다. 이를 막기 위해 openZeppelin의 safeMath 라이브러리를 사용한다.
using SafeMath for uint256;
uint256 a = 5;
uint256 b = a.add(3); // 5 + 3 = 8
uint256 c = a.mul(2); // 5 * 2 = 10
safeMath를 이용하여 ++,--를 add와 sub함수로 바꾸었다.
ownerZombieCount[_to] = ownerZombieCount[_to].add(1);
ownerZombieCount[msg.sender] = ownerZombieCount[msg.sender].sub(1);
using SafeMath32 for uint32;
using SafeMath16 for uint16;
솔리디티에서 표준으로 쓰이는 주석 형식은 natspec 이다. @title, @author @param @return등 주석으로 해당라인이 어떤의미인지 알려줄 수 있다.
/// @title 좀비 소유권 전송을 관리하는 컨트랙트
/// @author wook
/// @dev OpenZeppelin ERC721
이더리움 네트워크에서 사용자가 스마트 컨트랙트에게 질의할 수 있도록 해주는 라이브러리이다. JSON-RPC를 통해 소통한다.
Web3 Privider
infura
infura를 provider로 사용하면 이더리움 블록체인과 메시지를 쉽고 안전하게 주고받을 수 있다.
[출처] : (https://infura.io/)
metamask
이더리움 계정과 개인 키를 안전하게 관리할 수 있게 해주는 크롬과 파이어폭스의 브라우저 확장 프로그램이다.
metamask는 web3라는 전역 자바스크립트 객체를 통해 web3 provider를 주입한다. 그렇기 때문에 앱에서 web3의 존재여부를 확인하면 해당 provider를 사용하면 된다.
window.addEventListener('load', function() {
// Web3가 브라우저에 주입되었는지 확인(Mist/MetaMask)
if (typeof web3 !== 'undefined') {
// Mist/MetaMask의 프로바이더 사용
web3js = new Web3(web3.currentProvider);
} else {
// 사용자가 Metamask를 설치하지 않은 경우에 대해 처리
// 사용자들에게 Metamask를 설치하라는 등의 메세지를 보여줄 것
}
//앱을 시작
startApp()
})
var cryptoZombies;
function startApp() {
var cryptoZombiesAddress = "YOUR_CONTRACT_ADDRESS";
cryptoZombies = new web3js.eth.Contract(cryptoZombiesABI, cryptoZombiesAddress);
}
function zombieToOwner(id) {
return cryptoZombies.methods.zombieToOwner(id).call()
}
function getZombiesByOwner(owner) {
return cryptoZombies.methods.getZombiesByOwner(owner).call()
}
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) {
// 각각을 #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>`);
});
}
}
CryptoZombies.methods.createRandomZombie(name)
를 통해서 컨트랙트의 함수를 실행한다.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 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);
});
}
Event 구독하기
web3에서 컨트랙트 이벤트를 구독하기 위해서는 다음과 같은 코드를 작성하면 된다.
cryptoZombies.events.NewZombie().on()
...
하지만 이것은 새로운 좀비가 나타날 때 모든 이벤트에 대해서 구독하기 때문에 특정 사용자에 대한 이벤트만 받도록 제한해야 한다.
indexed
이벤트를 필터링하기 위해 사용된다.
event Transfer(address indexed _from, address indexed _to, uint256 _tokenId);
cryptoZombies.events.Transfer({ filter: { _to: userAccount } })
// filter를 통해서 사용자에 대한 Transfer만 받을 수 있다.
cryptoZombies.getPastEvents("NewZombie", { fromBlock: 0, toBlock: "latest" })
사용자가 송금을 받았을때 UI를 교체하는 함수이다.
cryptoZombies.events.Transfer({ filter: { _to: userAccount } })
.on("data", function(event) {
let data = event.returnValues;
getZombiesByOwner(userAccount).then(displayZombies);
}).on("error", console.error);
}