-
[이더리움 댑 코딩] CryptoZombies | Lesson 2Study/Blockchain 2022. 1. 18. 04:12
챕터 1: 레슨 2 개요
지난 레슨에서 좀비의 이름을 받아서 랜덤으로 좀비를 생성한 다음, 이 좀비를 블록체인상의 우리 앱 좀비 데이터베이스에 추가하는 함수를 만들었지.
이번 레슨에서는 우리 앱을 좀 더 게임답게 만들 걸세: 앱을 멀티플레이어 게임으로 만들고, 좀비를 랜덤으로만 생성하지 않고 좀더 재미있는 방식으로 좀비를 생성할 것이네.
어떻게 새로운 좀비를 생성할까? 좀비가 다른 생명체를 "먹도록" 해서!
좀비 먹이기
좀비가 먹이를 먹으면 먹이는 바이러스에 감염되지. 이 바이러스는 먹이를 새로운 좀비로 바꾸어 좀비 군대의 일원이 되도록 하지. 새로운 좀비의 DNA는 이전 좀비의 DNA와 먹이의 DNA를 활용하여 계산될 것이네.
그럼 우리 좀비들이 가장 좋아하는 먹이가 무엇일까?
그걸 알고 싶으면... 레슨 2를 마쳐야 할 것이네!
챕터 2: 매핑과 주소
데이터베이스에 저장된 좀비들에게 주인을 설정하여 우리 게임을 멀티 플레이어 게임으로 만들어 보세.
이걸 하려면 mapping과 address라는 2가지 새로운 자료형이 필요할 거네.
주소
이더리움 블록체인은 은행 계좌와 같은 계정들로 이루어져 있지. 계정은 이더리움 블록체인상의 통화인 _이더_의 잔액을 가지지. 자네의 은행 계좌에서 다른 계좌로 돈을 송금할 수 있듯이, 계정을 통해 다른 계정과 이더를 주고 받을 수 있지.
각 계정은 은행 계좌 번호와 같은 주소를 가지고 있네. 주소는 특정 계정을 가리키는 고유 식별자로, 다음과 같이 표현되지:
0x0cE446255506E92DF41614C46F1d6df9Cc969183
(이 주소는 크립토좀비 팀의 주소지. 자네가 크립토좀비를 즐기고 있다면 우리에게 이더 몇 개를 보내줄 수 있겠지! 😉)
이후 레슨에서 주소에 관한 핵심 내용을 알아 볼 것일세. 지금은 자네가 "주소는 특정 유저(혹은 스마트 컨트랙트)가 소유한다"라는 점만 이해하면 되네.
그러니까 주소를 우리 좀비들에 대한 소유권을 나타내는 고유 ID로 활용할 수 있네. 유저가 우리 앱을 통해 새로운 좀비를 생성하면 좀비를 생성하는 함수를 호출한 이더리움 주소에 그 좀비에 대한 소유권을 부여하지.
매핑
레슨 1에서 구조체와 _배열_을 살펴 봤네. _매핑_은 솔리디티에서 구조화된 데이터를 저장하는 또다른 방법이지.
다음과 같이 매핑을 정의하지:
// 금융 앱용으로, 유저의 계좌 잔액을 보유하는 uint를 저장한다: mapping (address => uint) public accountBalance; // 혹은 userID로 유저 이름을 저장/검색하는 데 매핑을 쓸 수도 있다 mapping (uint => string) userIdToName;
매핑은 기본적으로 키-값 (key-value) 저장소로, 데이터를 저장하고 검색하는 데 이용된다. 첫번째 예시에서 키는 address이고 값은 uint이다. 두번째 예시에서 키는 uint이고 값은 string이다.
챕터 3: Msg.sender
좀비 소유자를 추적하는 매핑을 가지고 있으니 _createZombie 메소드를 업데이트해서 이 매핑을 이용하도록 하고 싶네.
이를 위해, msg.sender라는 것을 이용할 필요가 있네.
msg.sender
솔리디티에는 모든 함수에서 이용 가능한 특정 전역 변수들이 있지. 그 중의 하나가 현재 함수를 호출한 사람 (혹은 스마트 컨트랙트)의 주소를 가리키는 msg.sender이지.
참고: 솔리디티에서 함수 실행은 항상 외부 호출자가 시작하네. 컨트랙트는 누군가가 컨트랙트의 함수를 호출할 때까지 블록체인 상에서 아무 것도 안 하고 있을 것이네. 그러니 항상 msg.sender가 있어야 하네.
msg.sender를 이용하고 mapping을 업데이트하는 예시가 여기에 있네:
mapping (address => uint) favoriteNumber; function setMyNumber(uint _myNumber) public { // `msg.sender`에 대해 `_myNumber`가 저장되도록 `favoriteNumber` 매핑을 업데이트한다 ` favoriteNumber[msg.sender] = _myNumber; // ^ 데이터를 저장하는 구문은 배열로 데이터를 저장할 떄와 동일하다 } function whatIsMyNumber() public view returns (uint) { // sender의 주소에 저장된 값을 불러온다 // sender가 `setMyNumber`을 아직 호출하지 않았다면 반환값은 `0`이 될 것이다 return favoriteNumber[msg.sender]; }
이 간단한 예시에서 누구나 setMyNumber을 호출하여 본인의 주소와 연결된 우리 컨트랙트 내에 uint를 저장할 수 있지.
msg.sender를 활용하면 자네는 이더리움 블록체인의 보안성을 이용할 수 있게 되지. 즉, 누군가 다른 사람의 데이터를 변경하려면 해당 이더리움 주소와 관련된 개인키를 훔치는 것 밖에는 다른 방법이 없다는 것이네.
챕터 4: Require
레슨 1에서 유저가 createRandomZombie를 호출하여 좀비 이름을 입력하면 새로운 좀비를 생성할 수 있도록 했네. 하지만, 만일 유저가 이 함수를 계속 호출해서 무제한으로 좀비를 생성한다면 게임이 매우 재미있지는 않을 걸세.
각 플레이어가 이 함수를 한 번만 호출할 수 있도록 만들어 보세. 이로써 새로운 플레이어들이 게임을 처음 시작할 때 좀비 군대를 구성할 첫 좀비를 생성하기 위해 createRandomZombie함수를 호출하게 될 것이네.
어떻게 하면 이 함수가 각 플레이어마다 한 번씩만 호출되도록 할 수 있을까?
이를 위해 require를 활용할 것이네. require를 활용하면 특정 조건이 참이 아닐 때 함수가 에러 메시지를 발생하고 실행을 멈추게 되지:
function sayHiToVitalik(string _name) public returns (string) { // _name이 "Vitalik"인지 비교한다. 참이 아닐 경우 에러 메시지를 발생하고 함수를 벗어난다 // (참고: 솔리디티는 고유의 스트링 비교 기능을 가지고 있지 않기 때문에 // 스트링의 keccak256 해시값을 비교하여 스트링 값이 같은지 판단한다) require(keccak256(_name) == keccak256("Vitalik")); // 참이면 함수 실행을 진행한다: return "Hi!"; }
sayHiToVitalik("Vitalik")로 이 함수를 실행하면 "Hi!"가 반환될 것이네. "Vitalik"이 아닌 다른 값으로 이 함수를 호출할 경우, 에러 메시지가 뜨고 함수가 실행되지 않을 걸세.
그러므로 require는 함수를 실행하기 전에 참이어야 하는 특정 조건을 확인하는 데 있어서 꽤 유용하지.
챕터 5: 상속
우리의 게임 코드가 꽤 길어지고 있군. 엄청나게 긴 컨트랙트 하나를 만들기 보다는 코드를 잘 정리해서 여러 컨트랙트에 코드 로직을 나누는 것이 합리적일 때가 있지.
이를 보다 관리하기 쉽도록 하는 솔리디티 기능이 바로 컨트랙트 _상속_이지:
contract Doge { function catchphrase() public returns (string) { return "So Wow CryptoDoge"; } } contract BabyDoge is Doge { function anotherCatchphrase() public returns (string) { return "Such Moon BabyDoge"; } }
BabyDoge 컨트랙트는 Doge 컨트랙트를 상속하네. 즉, 자네가 BabyDoge 컨트랙트를 컴파일해서 구축할 때, BabyDoge 컨트랙트가 catchphrase() 함수와 anotherCatchphrase() 함수에 모두 접근할 수 있다는 뜻이지. (Doge 컨트랙트에 정의되는 다른 어떤 public 함수가 정의되어도 접근이 가능하네)
상속 개념은 "고양이는 동물이다"의 경우처럼 부분집합 클래스가 있을 때 논리적 상속을 위해 활용할 수 있지. 하지만 동일한 로직을 다수의 클래스로 분할해서 단순히 코드를 정리할 때도 활용할 수 있지.
챕터 6: Import
와우! 우리가 방금 코드를 오른편으로 정리했다는 걸 알 수 있을 걸세. 이제 에디터의 상단부에 탭들이 있네. 탭들을 클릭해서 살펴보도록 하게.
우리 코드가 꽤 길어지고 있으니, 여러 파일로 나누어 정리하면 관리하기 더 편하겠지. 보통 이런 방식으로 솔리디티 프로젝트의 긴 코드를 처리할 것이네.
다수의 파일이 있고 어떤 파일을 다른 파일로 불러오고 싶을 때, 솔리디티는 import라는 키워드를 이용하지:
import "./someothercontract.sol"; contract newContract is SomeOtherContract { }
이 컨트랙트와 동일한 폴더에 (이게 ./가 의미하는 바임) someothercontract.sol이라는 파일이 있을 때, 이 파일을 컴파일러가 불러오게 되지.
챕터 7: Storage vs Memory
솔리디티에는 변수를 저장할 수 있는 공간으로 storage와 memory 두 가지가 있지.
Storage는 블록체인 상에 영구적으로 저장되는 변수를 의미하지. Memory는 임시적으로 저장되는 변수로, 컨트랙트 함수에 대한 외부 호출들이 일어나는 사이에 지워지지. 두 변수는 각각 컴퓨터 하드 디스크와 RAM과 같지.
대부분의 경우에 자네는 이런 키워드들을 이용할 필요가 없네. 왜냐면 솔리디티가 알아서 처리해 주기 때문이지. 상태 변수(함수 외부에 선언된 변수)는 초기 설정상 storage로 선언되어 블록체인에 영구적으로 저장되는 반면, 함수 내에 선언된 변수는 memory로 자동 선언되어서 함수 호출이 종료되면 사라지지.
하지만 이 키워드들을 사용해야 하는 때가 있지. 바로 함수 내의 구조체와 _배열_을 처리할 때지:
contract SandwichFactory { struct Sandwich { string name; string status; } Sandwich[] sandwiches; function eatSandwich(uint _index) public { // Sandwich mySandwich = sandwiches[_index]; // ^ 꽤 간단해 보이나, 솔리디티는 여기서 // `storage`나 `memory`를 명시적으로 선언해야 한다는 경고 메시지를 발생한다. // 그러므로 `storage` 키워드를 활용하여 다음과 같이 선언해야 한다: Sandwich storage mySandwich = sandwiches[_index]; // ...이 경우, `mySandwich`는 저장된 `sandwiches[_index]`를 가리키는 포인터이다. // 그리고 mySandwich.status = "Eaten!"; // ...이 코드는 블록체인 상에서 `sandwiches[_index]`을 영구적으로 변경한다. // 단순히 복사를 하고자 한다면 `memory`를 이용하면 된다: Sandwich memory anotherSandwich = sandwiches[_index + 1]; // ...이 경우, `anotherSandwich`는 단순히 메모리에 데이터를 복사하는 것이 된다. // 그리고 anotherSandwich.status = "Eaten!"; // ...이 코드는 임시 변수인 `anotherSandwich`를 변경하는 것으로 // `sandwiches[_index + 1]`에는 아무런 영향을 끼치지 않는다. 그러나 다음과 같이 코드를 작성할 수 있다: sandwiches[_index + 1] = anotherSandwich; // ...이는 임시 변경한 내용을 블록체인 저장소에 저장하고자 하는 경우이다. } }
어떤 키워드를 이용해야 하는지 정확하게 이해하지 못한다고 해도 걱정 말게. 이 튜토리얼을 진행하는 동안 언제 storage 혹은 memory를 사용해야 하는지 알려 주겠네. 솔리디티 컴파일러도 경고 메시지를 통해 어떤 키워드를 사용해야 하는지 알려 줄 것이네.
지금으로선 명시적으로 storage나 memory를 선언할 필요가 있는 경우가 있다는 걸 이해하는 것만으로 충분하네!
챕터 8: 좀비 DNA
feedAndMultiply 함수 작성을 마무리해 보세!
새로운 좀비의 DNA를 계산하는 공식은 간단하네: 먹이를 먹는 좀비의 DNA와 먹이의 DNA의 평균을 내는 거지.
예시:
function testDnaSplicing() public { uint zombieDna = 2222222222222222; uint targetDna = 4444444444444444; uint newZombieDna = (zombieDna + targetDna) / 2; // ^ 3333333333333333이 될 것이다 }
자네가 원한다면 나중에 공식을 좀더 복잡하게 할 수도 있을 거네. 하지만 지금으로선 공식을 간단하게 하도록 하지. 나중에 언제든지 변경할 수 있으니까.
챕터 9: 함수 접근 제어자 더 알아보기
지난 레슨의 코드에 실수가 있네!
자네가 코드를 컴파일하려고 하면 컴파일러가 에러 메시지를 출력할 거네.
문제는 ZombieFeeding 컨트랙트 내에서 _createZombie 함수를 호출하려고 했다는 거지. 그런데 _createZombie 함수는 ZombieFactory 컨트랙트 내의 private 함수이지. 즉, ZombieFactory 컨트랙트를 상속하는 어떤 컨트랙트도 이 함수에 접근할 수 없다는 뜻이지.
Internal과 External
public과 private 이외에도 솔리디티에는 internal과 external이라는 함수 접근 제어자가 있지.
internal은 함수가 정의된 컨트랙트를 상속하는 컨트랙트에서도 접근이 가능하다 점을 제외하면 private과 동일하지. **(우리한테 필요한 게 바로 internal인 것 같군!
external은 함수가 컨트랙트 바깥에서만 호출될 수 있고 컨트랙트 내의 다른 함수에 의해 호출될 수 없다는 점을 제외하면 public과 동일하지. 나중에 external과 public이 각각 왜 필요한지 살펴 볼 것이네.
interal이나 external 함수를 선언하는 건 private과 public 함수를 선언하는 구문과 동일하네:
contract Sandwich { uint private sandwichesEaten = 0; function eat() internal { sandwichesEaten++; } } contract BLT is Sandwich { uint private baconSandwichesEaten = 0; function eatWithBacon() public returns (string) { baconSandwichesEaten++; // eat 함수가 internal로 선언되었기 때문에 여기서 호출이 가능하다 eat(); } }
챕터 10: 좀비가 무엇을 먹나요?
이제 좀비들에게 먹이를 줄 시간이군! 좀비가 가장 좋아하는 먹이가 뭘까?
크립토좀비가 가장 좋아하는 먹이는...
크립토키티! 😱😱😱
(그래, 정말이라네 😆 )
좀비에게 크립토키티를 먹이로 주려면 크립토키티 스마트 컨트랙트에서 키티 DNA를 읽어와야 할 것이네. 이게 가능한 이유는 크립토키티 데이터가 블록체인 상에 공개적으로 저장되어 있기 때문이지. 블록체인이 환상적이지 않나?!
걱정 말게 - 우리 게임이 어느 누구의 크립토키티에게도 실제 해를 끼치지 않을 것이니 말일세. 우린 단지 크립토키티 데이터를 읽어 올 뿐이지. 실제로 이 데이터를 지울 수는 없다네. 😉
다른 컨트랙트와 상호작용하기
블록체인 상에 있으면서 우리가 소유하지 않은 컨트랙트와 우리 컨트랙트가 상호작용을 하려면 우선 인터페이스를 정의해야 하네.
간단한 예시를 살펴 보도록 하지. 다음과 같은 블록체인 컨트랙트가 있다고 해 보세:
contract LuckyNumber { 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 함수를 이용하여 이 컨트랙트에 있는 데이터를 읽고자 하는 external 함수가 있다고 해 보세.
먼저, LuckyNumber 컨트랙트의 인터페이스를 정의할 필요가 있네:
contract NumberInterface { function getNum(address _myAddress) public view returns (uint); }
약간 다르지만, 인터페이스를 정의하는 것이 컨트랙트를 정의하는 것과 유사하다는 걸 참고하게. 먼저, 다른 컨트랙트와 상호작용하고자 하는 함수만을 선언할 뿐(이 경우, getNum이 바로 그러한 함수이지) 다른 함수나 상태 변수를 언급하지 않네.
다음으로, 함수 몸체를 정의하지 않지. 중괄호 {, }를 쓰지 않고 함수 선언을 세미콜론(;)으로 간단하게 끝내지.
그러니 인터페이스는 컨트랙트 뼈대처럼 보인다고 할 수 있지. 컴파일러도 그렇게 인터페이스를 인식하지.
우리의 dapp 코드에 이런 인터페이스를 포함하면 컨트랙트는 다른 컨트랙트에 정의된 함수의 특성, 호출 방법, 예상되는 응답 내용에 대해 알 수 있게 되지.
다음 레슨에서 다른 컨트랙트의 함수를 실제로 호출할 것일세. 지금은 크립토키티 컨트랙트를 위한 인터페이스를 선언해 보세.
챕터 11: 인터페이스 활용하기
이전 챕터의 예시였던 NumberInterface를 활용하여 설명을 이어 나가겠네. 아래와 같이 인터페이스가 정의되면:
contract NumberInterface { function getNum(address _myAddress) public view returns (uint); }
다음과 같이 컨트랙트에서 인터페이스를 이용할 수 있지:
contract MyContract { address NumberInterfaceAddress = 0xab38... // ^ 이더리움상의 FavoriteNumber 컨트랙트 주소이다 NumberInterface numberContract = NumberInterface(NumberInterfaceAddress) // 이제 `numberContract`는 다른 컨트랙트를 가리키고 있다. function someFunction() public { // 이제 `numberContract`가 가리키고 있는 컨트랙트에서 `getNum` 함수를 호출할 수 있다: uint num = numberContract.getNum(msg.sender); // ...그리고 여기서 `num`으로 무언가를 할 수 있다 } }
이런 식으로 자네의 컨트랙트가 이더리움 블록체인상의 다른 어떤 컨트랙트와도 상호작용할 수 있네. 물론 상호작용하는 함수가 public이나 external로 선언되어 있어야 하지.
챕터 12: 다수의 반환값 처리하기
getKitty 함수는 우리가 살펴 본 예시 중 유일하게 다수의 반환값을 갖는 함수이지. 본 챕터에서는 어떻게 다수의 반환값을 처리하는지 살펴 보세:
function multipleReturns() internal returns(uint a, uint b, uint c) { return (1, 2, 3); } function processMultipleReturns() external { uint a; uint b; uint c; // 다음과 같이 다수 값을 할당한다: (a, b, c) = multipleReturns(); } // 혹은 단 하나의 값에만 관심이 있을 경우: function getLastReturnValue() external { uint c; // 다른 필드는 빈칸으로 놓기만 하면 된다: (,,c) = multipleReturns(); }
챕터 13: 보너스: 키티 유전자
우리의 함수 로직이 이제 완료되었군... 하지만 한 가지를 보너스로 추가해 보도록 하세.
고양이 유전자와 조합되어 생성된 좀비가 몇 가지 독특한 특성을 가져서 고양이 좀비로 보이도록 해 보세.
이를 위해 좀비 DNA에 몇 가지 특별한 키티 코드를 추가할 수 있네.
레슨 1에서 배운 내용을 떠올려 보면, 좀비의 외모를 결정하는 데 있어서 16자리 DNA 중에서 처음 12자리만 이용되지. 그러니 마지막에서 2자리 숫자를 활용하여 "특별한" 특성을 만들어 보세.
고양이 좀비는 DNA 마지막 2자리로 99를 갖는다고 해 보세 (고양이는 9개의 목숨을 가졌다고 할 만큼 생명력이 강하므로). 그러면 우리 코드에서는 만약(if) 좀비가 고양이에서 생성되면 좀비 DNA의 마지막 2자리를 99로 설정한다.
If 문
솔리디티에서 if 문은 자바스크립트의 if 문과 동일하다:
function eatBLT(string sandwich) public { // 스트링 간의 동일 여부를 판단하기 위해 keccak256 해시 함수를 이용해야 한다는 것을 기억하자 if (keccak256(sandwich) == keccak256("BLT")) { eat(); } }
챕터 14: 마무리하기 Wrapping It Up
그게 전부네. 레슨 2를 완료했네!
코드가 작동하는지 오른편에서 데모를 확인해 볼 수 있네. 자네가 얼른 이 페이지 하단으로 가고 싶어한다는 걸 아네 😉. 고양이를 클릭해서 공격하고 나서 새로운 고양이 좀비가 어떤지 보게!
자바스크립트를 활용한 구현
우리 컨트랙트를 이더리움에 구축할 준비가 되면 ZombieFeeding 컨트랙트만 컴파일해서 구축하면 될 것일세. 왜냐면 이 컨트랙트가 ZombieFactory를 상속하는 우리의 마지막 컨트랙트이고 두 컨트랙트에 있는 public 메소드를 모두 접근할 수 있기 때문이지.
자바스크립트와 web3.js를 활용하여 우리의 컨트랙트와 상호작용하는 예시를 살펴 보도록 하지:
var abi = /* abi generated by the compiler */ var ZombieFeedingContract = web3.eth.contract(abi) var contractAddress = /* our contract address on Ethereum after deploying */ var ZombieFeeding = ZombieFeedingContract.at(contractAddress) // 우리 좀비의 ID와 타겟 고양이 ID를 가지고 있다고 가정하면 let zombieId = 1; let kittyId = 1; // 크립토키티의 이미지를 얻기 위해 웹 API에 쿼리를 할 필요가 있다. // 이 정보는 블록체인이 아닌 크립토키티 웹 서버에 저장되어 있다. // 모든 것이 블록체인에 저장되어 있으면 서버가 다운되거나 크립토키티 API가 바뀌는 것이나 // 크립토키티 회사가 크립토좀비를 싫어해서 고양이 이미지를 로딩하는 걸 막는 등을 걱정할 필요가 없다 ;) let apiUrl = "https://api.cryptokitties.co/kitties/" + kittyId $.get(apiUrl, function(data) { let imgUrl = data.image_url // 이미지를 제시하기 위해 무언가를 한다 }) // 유저가 고양이를 클릭할 때: $(".kittyImage").click(function(e) { // 우리 컨트랙트의 `feedOnKitty` 메소드를 호출한다 ZombieFeeding.feedOnKitty(zombieId, kittyId) }) // 우리의 컨트랙트에서 발생 가능한 NewZombie 이벤트에 귀를 기울여서 이벤트 발생 시 이벤트를 제시할 수 있도록 한다: ZombieFactory.NewZombie(function(error, result) { if (error) return // 이 함수는 레슨 1에서와 같이 좀비를 제시한다: generateZombie(result.zombieId, result.name, result.dna) })
최종 코드는 아래와 같다.
zombiefeeding.sol
pragma solidity ^0.4.19; import "./zombiefactory.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 _species) public { require(msg.sender == zombieToOwner[_zombieId]); Zombie storage myZombie = zombies[_zombieId]; _targetDna = _targetDna % dnaModulus; uint newDna = (myZombie.dna + _targetDna) / 2; if (keccak256(_species) == keccak256("kitty")) { newDna = newDna - newDna % 100 + 99; } _createZombie("NoName", newDna); } function feedOnKitty(uint _zombieId, uint _kittyId) public { uint kittyDna; (,,,,,,,,,kittyDna) = kittyContract.getKitty(_kittyId); feedAndMultiply(_zombieId, kittyDna, "kitty"); } }
zombiefactory.sol
pragma solidity ^0.4.19; contract ZombieFactory { event NewZombie(uint zombieId, string name, uint dna); uint dnaDigits = 16; uint dnaModulus = 10 ** dnaDigits; struct Zombie { string name; uint dna; } Zombie[] public zombies; mapping (uint => address) public zombieToOwner; mapping (address => uint) ownerZombieCount; function _createZombie(string _name, uint _dna) internal { uint id = zombies.push(Zombie(_name, _dna)) - 1; zombieToOwner[id] = msg.sender; ownerZombieCount[msg.sender]++; NewZombie(id, _name, _dna); } function _generateRandomDna(string _str) private view returns (uint) { uint rand = uint(keccak256(_str)); return rand % dnaModulus; } function createRandomZombie(string _name) public { require(ownerZombieCount[msg.sender] == 0); uint randDna = _generateRandomDna(_name); randDna = randDna - randDna % 100; _createZombie(_name, randDna); } }
'Study > Blockchain' 카테고리의 다른 글
[이더리움 댑 코딩] CryptoZombies | Lesson 5 (2) 2022.01.20 [이더리움 댑 코딩] CryptoZombies | Lesson 4 (0) 2022.01.20 [이더리움 댑 코딩] CryptoZombies | Lesson 3 (0) 2022.01.20 [이더리움 댑 코딩] CryptoZombies | Lesson 1 (1) 2022.01.13