일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- useEffect clean up
- javascript기초
- 세션으로 로그인 구현
- express.static
- express router
- buffer.from
- 라우터 분리
- nodejs파일업로드
- 라우트 매개변수
- ws 라이브러리
- useContext
- FormData()
- Uncaught Error: could not find react-redux context value; please ensure the component is wrapped in a <Provider>
- 라우터미들웨어 분리
- 비동기파일업로드
- mysql wsl
- 라우터와 미들웨어
- cookie-parser 만들어보기
- css기초
- node.js path
- 시퀄라이즈 기본설정
- express session
- next 매개변수
- 블록 만들기
- OAuth 카카오
- 아이디 중복체크기능
- JWT 만들어보기
- JWT 하드코딩
- express실행
- JWT 로그인 기능 구현
- Today
- Total
즐코
Crypto Zombie / lesson2 본문
1. address
이더리움 네트워크 상에서의 계정을 가리킨다. 즉, 이더리움 주소 길이인 20바이트를 담을 수 있는 타입이다.
크립토 좀비에선 해당 address를 좀비들에 대한 소유권을 가진 고유 아이디값으로 쓴다.
2. mapping
struct, arrays 처럼 구조화된 데이터 타입을 저장하는데 쓰이는 또다른 데이터 타입이다. 객체처럼 key, value 값으로 이뤄져있다.
mapping (key => value) [변수명]
// 계정-그에 해당하는 잔고를 저장할 때, 계정,주소가 key값이고 uint 즉 잔고가 value
mapping (address => uint) public accountBalance;
// 사용자 아이디-그에 해당하는 실명을 저장할 때, 아이디가 key, string 즉 이름이 value
mapping (uint => string) userIdToName;
크립토 좀비에선 컨트랙트 내에 두 개의 mapping을 만들어준다.
하나는 특정 zombie를 소유하고 있는 계정 조회용으로 또 다른 하나는 계정마다 소유한 좀비수 조회용으로 만들어준다.
mapping(uint => address) public zombieToOwner;
mapping(address => uint) ownerZombieCount;
3. msg.sender
솔리디티에는 모든 함수에서 이용가능한 특정한 전역 변수들이 있다.
그 중 하나가 현재 함수를 호출한 사람의 주소 EOA (또는 스마트 컨트랙트 주소인 CA)를 나타내는 msg.sender 이다.
** 솔리디티에서 언제나 외부의 호출자에 의해서 함수가 실행된다. 즉, 컨트랙트는 블록체인 상에 배포되고 나서 누가 컨트랙트 내의 함수를 호출하지 않는 이상 그냥 가만히 있을 뿐이다. 아래 코드는 msg.sender 를 사용하여 mapping을 업데이트하는 예시이다.
참고로 mapping 데이터 저장/조회는 간단하다.
- 저장 : mapping명[key값] = 넣을 value값
- 조회 : mapping명[key값]
mapping (address => uint) favoriteNumber;
function setMyNumber(uint _myNumber) public {
favoriteNumber[msg.sender] = _myNumber;
// mapping으로 데이터를 저장할 시 위와 같은 문법을 사용한다.
}
function whatIsMyNumber() public view returns (uint) {
return favoriteNumber[msg.sender];
// sender 주소 (key)로 저장된 number(value)를 조회
// sender 가 setMyNumber를 아직 호출하지 않았다면 0을 리턴할 것이다.
}
이 msg.sender 를 활용하면 이더리움 블록체인의 보안성을 확인할 수 있다. 자신이 아닌 다른 누군가의 데이터를 수정하는 방법은 오로지 이더리움 주소와 연관된 개인키를 훔치지 않는 이상 수정이 불가능하다.
위의 내용을 바탕으로 _createZombie 함수를 수정해준다.
- 새로운 좀비의 아이디 (id) 를 얻게 되면, 이를 msg.sender 즉, 해당 함수를 호출한 사람의 주소를 key로 이 좀비 아이디를 저장하자.
- ownerZombieCount 즉, 해당 계정의 좀비 수를 증가시켜주자.
mapping (uint => address) public zombieToOwner;
mapping (address => uint) ownerZombieCount;
function _createZombie(string memory _name, uint _dna) private {
uint id = zombies.push(Zombie(_name, _dna)) - 1;
zombieToOwner[id] = msg.sender; // 좀비 아이디 : msg.sender
ownerZombieCount[msg.sender]++; // msg.sender : 좀비수
emit NewZombie(id, _name, _dna);
}
4. require
if문과 비슷하다. require 내의 조건문에 해당하지 않을 경우 함수를 실행하지 않고 에러를 던져준다.
크립토 좀비에서도 사용자가 좀비를 무한대로 생성하지 못하게끔 제한을 걸어줄 것이다. 보유한 좀비가 0개인 계정에만 좀비를 생성해줄 거임
* 두번째 인자로 에러메시지를 추가해주기도 한다!
require(ownerZombieCount[msg.sender] == 0, "Error ! - u've already created Zombie")
function createRandomZombie(string memory _name) public {
require(ownerZombieCount[msg.sender] == 0);
uint randDna = _generateRandomDna(_name);
_createZombie(_name, randDna);
}
5. 상속
한 컨트랙트를 길게 작성하는 것보단 여러 컨트랙트로 나누는 것이 효율적일 때가 있다.
class 를 상속할때 extends를 썼던 것처럼 contract는 is 라는 키워드를 써준다.
부모(상속해오는 컨트랙트) 컨트랙트 내의 public 함수 및 속성, 상태변수들에 접근이 가능해진다.
* 이때 다른 컨트랙트를 가져오려면 import 구문을 아래와 같이 작성해주면 끝!
import "./ZombieFactory.sol";
contract ZombieFeeding is ZombieFactory{
}
6. data location - storage vs memory
솔리디티에는 변수를 저장하는 공간이 2개 있는데 storage 와 memory이다.
- storage : 블록체인 상에 영구적으로 저장되는 변수 (컴퓨터 하드디스크와 같다)
- memory : 일시적으로 저장되며, 컨트랙트 함수의 외부 호출들이 일어나는 사이에 사라진다. (컴퓨터의 RAM과 같다)
다행인건 우리가 일일이 이 키워드들을 변수 앞에 붙여주지 않아도 솔리디티가 자체적으로 처리해준다!
함수 외부에 선언된 변수인 상태 변수의 경우 디폴트가 storage로 선언되기 때문에 블록체인 상에 영구적으로 저장되는 반면,
함수 내에서 선언된 변수는 자동으로 memory로 선언되어서 함수 호출이 종료되면 사라진다.
하지만, 이 키워드들을 붙여줘야할때가 있는데, 함수 내에서 structs와 arrays 타입을 다룰 때이다.
Contract SandwichFactory{
struct Sandwich{
string name;
string status;
}
Sandwich[] sandwiches;
function eatSandwich(uint _index) public {
// Sandwich mySandwich = sandwiches[_index];
// 이렇게 할 경우 경고메시지를 준다. storage, memory 또는 calldata 중 하나를 써줘야한다고,,
Sandwich storage mySandwich = sandwiches[_index];
// mySandwich는 sandwiches[_index]를 가리키는 포인터이다.
mySandwich.status = "Eaten!";
// storage로 선언했기 때문에 블록체인 상에서 sandwiches[_index]를 영구적으로 변경한다.
// 그냥 단순하게 복사만 하고 싶다면 memory를 사용해준다.
Sandwich memory anotherSandwich = sandwiches[_index+1];
// anotherSandwich는 단순히 메모리에 데이터를 복사하는 것이 됨
anotherSandwich.status = "Eaten!";
// 이 경우 anotherSandwich를 변경한다고 해서,
// sandwiches[_index+1] 에는 아무런 영향을 끼치지않는다.
sandwiches[_index + 1] = anotherSandwich;
// 이경우엔 변경된 내용을 블록체인 네트워크 상에 저장된다.
}
}
크립토좀비 스토리(?)상 먹이를 먹었을 때 그 먹이의 dna와 먹이를 먹는 좀비의 dna를 조합해서 새로운 좀비를 만들것이기 때문에, 먹는 좀비의 dna를 가져와야한다. 이는 미리 만들어놨던 zombies 배열에 담겨있을 것이다.
1/ 함수 feedAndMultiply 호출자 (msg.sender)가 해당 좀비를 소유하고 있는 사람인지 확인을 거쳐야하기때문에 require문을 썼다.
2/ zombieId로 zombies 배열에서 가져온 특정 좀비(먹잇감을 줄 좀비)를 storage에 저장한다
3/ 가져온 좀비의 dna (myZombie.dna) 와 먹잇감의 dna(_targetDna)의 평균값을 새로운 DNA 라고 친다.
* 이 때 _targetDna가 몇자리 숫자가 들어올지 모르므로 16자릿수로 맞춰주는 작업이 미리 필요하다.
4/ 부모 컨트랙트의 ZombieFactory의 _createZombie 함수를 사용하여 새로운 좀비를 만들어준다.
// contracts/ZombieFeeding.sol
import "./ZombieFatory.sol";
contract ZombieFeeding is ZombieFactory {
function feedAndMultiply(uint _zombieId, uint _targetDna) public {
require(msg.sender == zombieOwner[_zombieId]);
Zombie storage myZombie = zombies[_zombieId]);
_targetDna = _targetDna % dnaModulus; // 받은 dna를 16자릿수로 맞춰주기 위해서
uint newDna = (myZombie.dna + _targetDna) / 2;
_createZombie("NoName", newDna);
}
}
다만, zombieFactory.sol의 _createZombie는 private함수로 설정해놨다. 그렇기 때문에, ZombieFeeding 컨트랙트가 아무리 상속받은 컨트랙트여도 private 함수를 가져와 호출하는 건 불가능하다. 이때 등장하는 개념이 또다른 함수접근제한자 키워드인 internal, external 이다.
7. internal vs external
저번에는 public private 두 개의 함수 접근 제어자를 배웠다. 이번엔 internal과 external을 가볍게 정리해본다.
- internal : private과 유사하지만, 상속하는 contract 에서도 접근이 가능하다.
- external : public과 유사하지만, 오직 컨트랙트 외부에서만 호출이 가능하다. 따라서, 컨트랙트 내부의 다른 함수들에선 이 external 함수를 호출할 수 없다고 한다.
8. interface
1/ 인터페이스 정의
나의 컨트랙트가 블록체인 상의 또다른 컨트랙트와 상호작용하려면 interface를 정의해줘야한다.
아래의 예시를 보면, 내 행운의 숫자를 내 이더리움 주소 상에 저장할 수 있는 간단한 컨트랙트이다.
contract LuckNumber {
mapping(address => uint) numbers;
function setNum(uint _num) public {
numbers[msg.sender] = _num;
}
function getNum(address _myAddress) public view returns (uint) {
return numbers[_myAddress];
}
}
getNum이라는 public 함수를 통해서 누구나 특정 계정의 행운의 숫자를 조회할 수 있다. 이 때 이 LuckyNumber 컨트랙트의 인터페이스를 정의해줘야 한다.
contract 정의하듯이 키워드 contract를 붙여주고,
그 내부에서 다른 컨트랙트와 상호작용하고자 하는 함수를 선언만 해주고, 함수 바디 부분인 { } 는 빼주고 ;으로 마무리해주면된다.
contract NumberInterface {
function getNum(address _myAddress) public view returns (uint);
}
이때, 상호작용하는 함수는 public 이나 external로 선언되어있어야한다.
크립토 좀비에선 크립토 키티를 먹기 때문에 크립토키티 스마트 컨트랙트에서 키티의 DNA를 읽어올 것이다.
크립토키티의 소스 코드 상에는 모든 키티 데이터 (특히 gene)를 반환하는 getKitty라는 external 함수가 있어 이를 이용할 것이다.
// contracts/ZombieFeeding.sol
contract KittyInterface {
function getKitty(uint256 _id) external view returns (
bool isGestating,
bool isReady,
uint256 cooldownIndex,
uint256 nextActionAt,
uint256 siringWithId,
uint256 birthTime,
uint256 matronId,
uint256 sireId,
uint256 generation,
uint256 genes
);
}
2/ 인터페이스 사용
컨트랙트 내에서 예제로 만든 NumberInterface 를 사용해보려고 한다.
인터페이스를 통해 블록체인 내 다른 컨트랙트와 상호작용할 수 있다.
contract MyContract {
address NumberInterfaceAddress = 0xab382a312 ...
// 이전에 예시로 든 FavoriteNumber 컨트랙트의 주소값 (CA)을 가져온 것이다.
NumberInterface numberContract = NumberInterface(NumberInterfaceAddress);
// numberContract 는 다른 컨트랙트를 가리키고 있다.
function someFunction() public {
// numberContract가 가리키고 있는 컨트랙트에서 getNum을 실행시킬 수 있다.
uint num = numberContract.getNum(msg.sender);
}
}
크립토키티 인터페이스로 크립토 키티에서 getKitty 함수를 가져와 사용해본다.
이때 해당 크립토 키티 컨트랙트에 접근하려면 크립토키티 컨트랙트 주소가 있어야하는데, 크립토좀비 게임쪽에서 제공해준다.
이를 interface의 인자로 넣으면 getKitty 함수에 접근이 가능해진다!
// contracts/ZombieFeeding.sol
contract KittyInterface {
function getKitty(uint256 _id) external view returns (
bool isGestating,
bool isReady,
uint256 cooldownIndex,
uint256 nextActionAt,
uint256 siringWithId,
uint256 birthTime,
uint256 matronId,
uint256 sireId,
uint256 generation,
uint256 genes
);
}
contract ZombieFeeding is ZombieFactory{
address ckAddress = 0x06012c8cf97BEaD5deAe237070F9587f8E7A266d;
KittyInterface kittyContract = KittyInterface(ckAddress);
// 위의 ckAddress라는 크립토키티 컨트랙트 주소에 접근하여서 getKitty 함수 실행이 가능하다.
function feedAndMultiply(uint _zombieId, uint _targetDna) public {
require(msg.sender == zombieToOwner[_zombieId]);
Zombie storage myZombie = zombies[_zombieId];
uint newDna = (myZombie.dna + (_targetDna % dnaModulus)) / 2;
_createZombie("NoName", newDna);
}
function feedOnKitty(uint _zombieId, uint _kittyId) public {
uint kittyDna;
(,,,,,,,,,kittyDna) = kittyContract.getKitty(_kittyId);
// 여러 개의 반환값 중 마지막 genes속성만 가져온 것이다.
feedAndMultiply(_zombieId, kittyDna);
}
}
** 한 개 이상의 return값을 가지는 함수일 경우 아래와 같이 처리할 수 있다.
function multipleReturn() internal returns (uint a, uint b, uint c){
return (1, 2, 3);
}
function getAllReturns() external {
uint a;
uint b;
uint c;
// 여러개 반환값 가져올 때
(a, b, c) = multipleReturn();
}
function getLastReturn() external {
uint c;
// 가져오지 않아도 되는 값은 빈값으로 처리하면 된다.
(,,c) = multipleReturn();
}
추가적인 기능!
새로운 좀비 생성 시 크립토키티를 먹었을 경우엔 고양이좀비로 만들어주자!
if 문을 써서 크립토 키티를 먹었을 경우엔, 새로 생성된 좀비 dna 의 맨 끝 2자리를 99로 만들어준다.
// contracts/ZombieFeeding.sol
contract KittyInterface {
function getKitty(uint256 _id) external view returns (
bool isGestating,
bool isReady,
uint256 cooldownIndex,
uint256 nextActionAt,
uint256 siringWithId,
uint256 birthTime,
uint256 matronId,
uint256 sireId,
uint256 generation,
uint256 genes
);
}
contract ZombieFeeding is ZombieFactory{
address ckAddress = 0x06012c8cf97BEaD5deAe237070F9587f8E7A266d;
KittyInterface kittyContract = KittyInterface(ckAddress);
// 먹이를 줄때, 어떤 종을 먹는지도 세번째 인자로 받아오자..
function feedAndMultiply(uint _zombieId, uint _targetDna, string memory _species) public {
require(msg.sender == zombieToOwner[_zombieId]);
Zombie storage myZombie = zombies[_zombieId];
uint newDna = (myZombie.dna + (_targetDna % dnaModulus)) / 2;
// 직접적으로 string을 keccak25 해시함수에 넣을 수 없으므로 바이트화 시켜준다.
if(keccak256(abi.encodePacked(_species)) == keccak256(abi.encodePacked("kitty"))){
newDna = newDna - newDna % 100 + 99;
// 세번째 인자로 받은 종이 "kitty"일 경우, dna 끝 2자리를 없애고 99를 붙여준다.
}
_createZombie("NoName", newDna);
}
function feedOnKitty(uint _zombieId, uint _kittyId) public {
uint kittyDna;
(,,,,,,,,,kittyDna) = kittyContract.getKitty(_kittyId);
// 고양이 밥을 줄 경우, 세번째 인자로 "kitty" 추가
feedAndMultiply(_zombieId, kittyDna, "kitty");
}
}
만들어진 컨트랙트를 이더리움에 배포할 준비가 되면, ZombieFeeding.sol 파일만 컴파일하고 배포하면 된다!
왜냐하면 이 ZombieFeeding 컨트랙트가 ZombieFactory 컨트랙트를 상속해서, 내부의 모든 public 함수에 접근 가능하기 때문이다!
자바스크립트, web3 활용
아래 코드는 제대로 돌아가는 코드는 아니다. 자바스크립트와 web3를 사용해서 컨트랙트를 가져와 실행시키는 등의 상호작용을 할 때 이런 흐름으로 간다는 걸 보여주기 위함이다.
import ZombieContract from "./contracts/ZombieFeeding.json";
const abi = ZombieContract.abi;
const ZombieFeedingCont = web3.eth.contract(abi);
const networkId = web3.eth.net.getId(); // 물론 async-await 처리 해줘야하지만 생략한다..
const ca = ZombieContract.networks[networkdId].address;
const ZombieFeeding = ZombieFeedingCont.at(ca);
// 좀비 아이디, 먹잇감(target) 고양이 아이드를 가지고 있다고 가정할 시,
let zombieId = 1;
let kittyId = 1;
// 크립토키티 이미지를 얻기 위해 웹 api에 요청해야하는데,,
let apiUrl = "https://api.cryptokitties.co/kitties/" + kittyId
let response = axios.get(apiUrl)
// response.data를 가지고 do something...
// 유저가 고양이 클릭시,
const onClick = () => {
ZombieFeeding.feedOnKitty(zombieId, kittyId);
}
// 고양이를 먹고 새 좀비를 생성했을 때 이벤트 listen,
ZombieFactory.NewZombie((err,result)=>{
if (err) return;
// 새로운 좀비 result (zombieId, name, dna) 를 가지고 뭔가 화면상에 나타낼 수 있겠다..
})
'BlockChain' 카테고리의 다른 글
스마트 컨트랙트로 초간단 투표 dApp 만들기 (0) | 2022.07.20 |
---|---|
스마트 컨트랙트로 토큰 발행 및 전송해보기 (0) | 2022.07.19 |
Crypto Zombie / lesson1 - Overview (0) | 2022.07.18 |
스마트 컨트랙트 이벤트 등록 / 백엔드에서 트랜잭션 생성 (0) | 2022.07.15 |
메타마스크 통해서 스마트 컨트랙트 실행해보기 (0) | 2022.07.13 |