본문 바로가기

Ethereum/Solidity

CrytoZombies_Lesson2_심화문법

1. 매핑과 주소

 

(1)주소

이더리움 블록체인은 은행 계좌와 같은 계정들로 이루어져있습니다. 계정은 이더리움 블록체인상의 통화인 '이더'의 잔액을 갖고있습니다. 은행 계좌에서 다른 계좌로 돈을 송금할 수 있듯, 계정을 통해 다른 계정과 이더를 주고 받을 수 있습니다. 주소는 특정 계정을 가리키는 고유 식별자로 다음과 같이 표현됩니다.

0x0cE446255506E92DF41614C46F1d6df9Cc969183

 

(2)매핑

매핑은 키-값(key-value)저장소로, 데이터를 저장하고 검색하는데 이용됩니다. 

 

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) private {
        uint id = zombies.push(Zombie(_name, _dna)) - 1;
        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 {
        uint randDna = _generateRandomDna(_name);
        _createZombie(_name, randDna);
    }

}

zombieToOwner라는 매핑의 키는 uint이고 값은 address입니다. 이 매핑을 public으로 설정했습니다.

ownerZombieCount라는 매핑의 키는 address이고 값은 unit입니다. 

 

(3)msg.sender

솔리디티에는 모든 함수에서 이용 가능한 특정 전역 변수들이 있습니다. 그 중의 하나가 현재 함수를 호출한 사람의 주소를 가리키는 msg.sender입니다. 

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) private {
        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 {
        uint randDna = _generateRandomDna(_name);
        _createZombie(_name, randDna);
    }

}

uint id = zombies.push(Zombie(_name, _dna)) - 1;

->array.push()는 배열의 새로운 길이를 uint형으로 반환합니다. 배열의 첫원소가 0이라는 인덱스를 갖기 때문에, array.push() -1은 방금 추가한 값의 인덱스가 될것입니다. 

 

zombieToOwner[id] = msg.sender;

-> id값을 key로하여msg.sender 즉, 현재 함수를 호출한 사람의 주소를 저장합니다. 이를 시각화 하면 다음과 같습니다.

id (key) address (value)
0 0x0cE446255506E92DF41614C46F1d6df9Cc969183

id값만 알면 msg.sender의 주소를 찾을 수 있게 되었습니다. 

 

ownerZombieCount[msg.sender]++;

->저장된 msg.sender을 고려하여 ownerZombieCount을 1 증가시킵니다. 이를 시각화하면 다음과 같습니다.

address (key) uint (value)
0x0cE446255506E92DF41614C46F1d6df9Cc969183 1

address를 알면 unit값이 얼마인지 알 수 있습니다.

 

NewZombie(id, _name, _dna);

-> NewZombie 이벤트를 실행하여 앱에게 _createZombie함수가 실행되었음을 알립니다.

 

3. Require

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) private {
        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);
        _createZombie(_name, randDna);
    }

}

createRandomZombie함수를 호출하고 실행시기 위해서 ownerZombieCount[msg.sender]가 0인지 아닌지를 확인한수 0인경우만 해당 함수를 실행하도록 합니다. require는 '( )' 안의 값이 true 인 경우에만 해당 함수를 실행하게 됩니다. 

 

4. 상속

코드가 길어지는 경우 긴 컨트랙트 하나를 만들기 보다는 코드를 정리해서 여러 컨트랙트에 코드 로직을 나누는것이 합리적인 경우가 있습니다. 이를 보다 관리하기 쉽도록 하는 솔리디티 기능이 컨트랙트 '상속'입니다. 상속을 하게 되면 상속받은 컨트랙트는 따로 함수를 작성하지 않더라도 상속해준 컨트랙트의 함수를 이용할 수 있습니다. 보통 긴 코드를 여러개의 파일로 나누어 정리하게 되는데 이 경우에는 import라는 키워드를 사용해야 합니다. 

 

pragma solidity ^0.4.19;

import "./zombiefactory.sol";

contract ZombieFeeding is ZombieFactory {

}

 

지금까지했던 cryptoZombies 코드를 zombiefactory.sol에 저장하고 이를 상속받는 zombiefeeding.sol(새로운 컨트랙트 파일)을 만듭니다. zombiefeeding.sol이 zombiefactory.sol를 상속받기 위해  import "./zombiefactory.sol"; 라는 코드를 추가했습니다. 이제 zombiefeeding.sol은 zombiefactory.sol에 있는 함수에 접근 할 수 있게 되었습니다.

 

5. Storage / Memory

 

솔리디티에는 변수를 저장할 수 있는 공간으로 Storage와 Memory 두가지가 있습니다. Storage는 블록체인 상에 영구적으로 저장되는 변수를 의미합니다. Memory는 임시적으로 저장되는 변수로, 컨트랙트 함수에 대한 외부 호출들이 일어나는 사이에 지워지게 됩니다. 

 

pragma solidity ^0.4.19;

import "./zombiefactory.sol";

contract ZombieFeeding is ZombieFactory {

  function feedAndMultiply(uint _zombieId, uint _targetDna) public {
    require(msg.sender == zombieToOwner[_zombieId]);
    Zombie storage myZombie = zombies[_zombieId];
  }

}

feedAndMultiply라는 함수를 생성합니다. 이 함수는 uint형인 _zombieId  _targetDna을 전달받게 됩니다. 이 함수는 msg.sender의 주소와 zombieToOwner에 _zombieId를 넣어 나온 주소와 같은 경우에만 실행되도록 require를 설정했습니다. Zombie형 변수 myZombie에는 zombies배열에서 _zimbieId가 인덱스값은 좀비의 name과 Dna 값을 저장하도록 합니다. 

 

6. 새로운 dna값 구하기

pragma solidity ^0.4.19;

import "./zombiefactory.sol";

contract ZombieFeeding is ZombieFactory {

  function feedAndMultiply(uint _zombieId, uint _targetDna) public {
    require(msg.sender == zombieToOwner[_zombieId]);
    Zombie storage myZombie = zombies[_zombieId];
    _targetDna = _targetDna % dnaModulus;
    uint newDna = (myZombie.dna + _targetDna) / 2;
    _createZombie("NoName", newDna);
  }

}

_targetDna가 16자리를 넘지 않도록 10^16(dnaModulus)로 나누어 나머지만 취하도록 합니다. 그 다음 newDna라는 변수에 myZombie에 저장한 dna값과 함수가 전달받은 dna값의 평균값을 구하도록합니다. 새로운 dna값을 얻게 되면 _createZombie 함수를 호출합니다. 이 함수는 이름을 인자값으로 필요로하지만 현재는 "Noname"으로 하도록합니다. 

 

7. 함수 접근 제어자 (internal/external)

 

위의 6에서 _createZombie함수를 호출하려고 했는데, _createZombie함수는 ZombieFactory 컨트랙트 내의 private 함수이므로 오류가 발생할 것입니다. 따라서 이를 해결하기위하여 internal 함수제어자를 사용해야 합니다. 

 

internal은 함수가 정의된 컨트랙트를 상속하는 컨트랙트에서도 접근이 가능한 private 함수라고 생각하면 됩니다.

external은 함수가 컨트랙트 바깥에서만 호출될 수 있고 컨트랙트 내의 다른 함수에 의해 호출될 수 없다는 점을 제외하면 public함수와 동일하다고 생각하면 됩니다. 

 

8. 다른 컨트랙트와 상호작용하기

 


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) public {
    require(msg.sender == zombieToOwner[_zombieId]);
    Zombie storage myZombie = zombies[_zombieId];
    _targetDna = _targetDna % dnaModulus;
    uint newDna = (myZombie.dna + _targetDna) / 2;
    _createZombie("NoName", newDna);
  }

}

 

이더리움상의 크립토키티 스마트 컨트랙트와 상호작용하기 위해서 크립토키티 컨트랙트를 내가 만들고 있는 컨트랙트파일안에 인터페이스 정의를 해야 합니다. 이때 getkitty 함수에서 returns()까지만 복사한 후 ;(세미콜론)으로 끝을 냅니다. 그 다음 ZombieFeeding 컨트랙트 안에  이더리움 블록체인 상의 크립토 키티 스마트컨트랙트의 주소를 정의 하고 그 밑에 kittyContract라는 KittyInterface를 생성합니다. 

 

9. 다수의 반환값 처리하기

 


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) public {
    require(msg.sender == zombieToOwner[_zombieId]);
    Zombie storage myZombie = zombies[_zombieId];
    _targetDna = _targetDna % dnaModulus;
    uint newDna = (myZombie.dna + _targetDna) / 2;

    _createZombie("NoName", newDna);
  }

  function feedOnKitty(uint _zombieId, uint _kittyId) public {
    uint kittyDna;
    (,,,,,,,,,kittyDna) = kittyContract.getKitty(_kittyId);
    feedAndMultiply(_zombieId, kittyDna);
  }

}

feedOnKitty라는 함수를 생성합니다. 이 함수는 _zombieId _kittyId라는 uint 인자 값 2개를 전달받습니다. 먼저 kittyDna라는 uint를 선언하도록 합니다. 우리가 원하는 것은 마지막 변수인 genes이므로 앞의 값은 ,(쉼표)로 처리하여 받환받지 않도록 한다. (만약 우리가 sireId에도 관심이 있는 경우에는 kittysireId라고 uint를 선언한후 (,,,,,,,kittysireId,kittyDna)=kittyContract.getKitty(_kittyId); 라고 하면 될것입니다.)

'Ethereum > Solidity' 카테고리의 다른 글

Solidity _(1)Voting Program  (0) 2019.07.24
CrytoZombies_Lesson3_고급 솔리디티 개념  (0) 2019.07.23
CrytoZombies_Lesson1_기본문법  (0) 2019.07.23
CryptoZombies_Epilogue  (0) 2019.07.23