Geth 설치 및 로컬에 세팅 (윈도우 기준)

로컬 테스트넷에서 Geth 를 가동하기 위해선 두가지를 준비해야함

  1. 데이터 디렉터리 ( chaindata )
  2. Genesis.json 파일

윈도우 기반에서 진행한다.

  1. 윈도우로 GETH 설치를 진행하자. https://geth.ethereum.org/downloads/
  2. genesis.json 을 만들자.
  3. {
      "coinbase"   : "0x0000000000000000000000000000000000000001",
      "difficulty" : "0x20000",
      "extraData"  : "",
      "gasLimit"   : "0x8000000",
      "nonce"      : "0x0000000000000042",
      "mixhash"    : "0x0000000000000000000000000000000000000000000000000000000000000000",
      "parentHash" : "0x0000000000000000000000000000000000000000000000000000000000000000",
      "timestamp"  : "0x00",
      "alloc": {},
      "config": {
            "chainId": 15,
            "homesteadBlock": 0,
            "eip155Block": 0,
            "eip158Block": 0
        }
    }
    
  4. 하위에 chaindata 폴더를 만들어서 저장할 곳을 만들자.

그리고 geth로 초기화를 진행한다.

geth --datadir=chaindata/ init genesis.json

//macos
geth --datadir=./chaindata/ init ./genesis.json

그러면 chaindata 디렉토리 아래에는 블록에 대한 정보가 보이며

keystore 아래에는 계정에 관한 정보가 보일것이다.

-geth - chinadata, lightchaindata
-keystore

그럼 실행해보자.

geth --networkid 4649 --nodiscover --maxpeers 0 --datadir chaindata console 2>> chaindata/geth.log

-- networkdid : 네트워크 식별자로 쓰인다. 0~3까지는 예약어이다.

0 = onlympic, 1= Frontier, 2=morden(disused), 3=Ropsten 기본값은 1이다.

-- nodiscover : 생성자의 노드를 다른 노드에서 검색할 수 없게 하는 옵션이다. 노드 추가는 수동으로 해야함,

-- maxpeers 0 : 생성자의 노드에 연결할 수 있는 노드의 수를 지정함, 0은 다른 노드뢍 연결안함

-- datadir 데이터 디렉토리 저정

console 콘솔로 진입한다.

2>> 데이터 디렉토리/geth.log 로그 파일 만들때 사용하는 옵션 리눅스 셀 명령어

이더리움에서는 계정이 두가지로 나뉜다.

  • EOA (Externally Owned Account) 일반 사용자가 사용하는 계정이고 Ether를 송금하거나 계약 실행시 필요한 계정
  • Contract 계정은 계약을 배포시 만들어지는 나오는 계정으로 블록체인에 존재함

EOA 계정생성 (콘솔에서)

> personal.newAccount("pass0")
"0x295e3893ed0cd5fb04fbfb4bba656f509ac21aff" //계정 주소

계정 정보 확인

> eth.accounts
["0x295e3893ed0cd5fb04fbfb4bba656f509ac21aff"]

exit를 하면 geth 가 자동 중지가 된다.

> exit

그럼 채굴을 해보자.

우선 다시 콘솔로 진입해보자.

geth --networkid 4649 --nodiscover --maxpeers 0 --datadir chaindata console 2>> chaindata/geth.log
> eth.coinbase
"0x295e3893ed0cd5fb04fbfb4bba656f509ac21aff"

coinbase는 채굴에 성공시 보상을 받는 계정을 조회하는 명령어이다.

만약 다른 계정으로 하고 싶을땐

miner.setEtherbase 로 가능하다.

> eth.accounts
["0x295e3893ed0cd5fb04fbfb4bba656f509ac21aff", "0x65682c210e9229e5616f1a49f710ce1b4577688b", "0xa817f884ebc1d181dbb3c35c
1cbdeae9b36221c5"]
> eth.coinbase
"0x295e3893ed0cd5fb04fbfb4bba656f509ac21aff"
> miner.setEtherbase(eth.accounts[1])
true // 성공됨
> eth.coinbase
"0x65682c210e9229e5616f1a49f710ce1b4577688b" //2번째 계정으로 지정됨

다시 첫번째 계정으로 돌려 놓자.

> miner.setEtherbase(eth.accounts[0])
true

잔고확인도 해보자.

> eth.getBalance(eth.accounts[0])
0
> eth.getBalance(eth.accounts[1])
0
> eth.getBalance(eth.accounts[2])
0

블록 수 확인도 가능하다. 아직 블록을 생성하지 않았으므로 0 이다.

> eth.blockNumber
0

그러면 채굴을 시작해보자.

miner.start(threadnum) 로 명령어 사용가능하다. 여기에서 thread_num은 채굴시 사용하는 쓰레드 수이다. 우선 1로 설정

> miner.start(1)
null or true
> miner.mining //마이닝 되고 있는 지 체크
true
> eth.hashreate
1086190 or 0
> eth.blockNumber
61

그리고 miner.stop 을 해보자.

> miner.stop()
//계좌 확인
> eth.getBalance(eth.coinbase)
125000000000000000000
> eth.getBalance(eth.accounts[1]) //현재 1로 설정되어 있는 상태입니다.
125000000000000000000
> eth.getBalance(eth.accounts[0])
95000000000000000000

0이 엄청 많아 보이지만 최소 단위인 wei 로 보여서 그렇다. 10^18승이 1이더이다. 그럼 이더로 변환하면

> web3.fromWei(eth.getBalance(eth.accounts[1]),"ether")
125 // 이더로 변환

다시 한번 블록 갯수 체크해본다.

> eth.blockNumber
44

블록당 보상금액이 5ether이다. 그래서 계정 2개 보상 금액 합치면 220 ( 125 + 95 ) 이더이다. 즉 220 = 5 * 44 로 맞아떨어진다.

이렇게 보상금액을 계산을 가능하다.


Truffle를 활용한 Pet-Shop 튜터리얼 분석

광고 클릭시 많은 힘이 됩니다. 


Truffle 튜터리얼 중 펫샵을 활용해서 프론트 까지 적용해서 어떤식으로 웹에서 연동되는지 살펴보자.

아래 주소에 있는 내용은 깔끔하게 정리되어 있다.  따라해보는 걸 추천한다. 

여기 글은 펫샵에 사용되는 소스를 분석해보자 한다. 

이더리움 플랫폼 위 DAPP개발 프레임워크 중 하나인 truffle사용시 개발시 순서는 다음과 같다.

  • 개발환경을 설정한다.
  • truffle box( 보일러 플레이트처럼 만들어준다.)를 이용해서 pet-shop 을 해제
  • 스마트 계약 소스를 (.sol) 작성한다.
  • 컴파일 및 배포를 한다.
  • Ganeche 를 통해 테스팅을 한다.
  • 웹 브라우저에서 스마트 계약이 정상적으로 인터랙션이 제대로 이루어지는지 확인해본다.

아래 이미지는 결과 화면이다. Adopt 버튼을 통해서 success 이 제대로 이루어 지는 지 보면 된다.

MetaMask 를 통해서 창이 열리면서 Submit를 클릭해준다.

최종적으로 채택이 된 것을 웹에서 확인해볼 수 있다. ( 새로고침을 해야한다. )

결과물은 따라하다보면 쉽게 볼 수 있을것이다. 여기서 특이한 점은

MetaMask를 통한 Custom RPC 로 ganache private network 로 접속해서 진행

스마트 계약을 테스팅시 솔리디티 언어로 테스팅 하는 방법

우선 Adoption.sol 파일을 훝어보자.

pragma solidity ^0.4.17;

contract Adoption {
    address[16] public adopters;

    function adopt(uint petId) public returns (uint) {
        require(petId >= 0 && petId <= 15);

        adopters[petId] = msg.sender;

        return petId;
    }

    function getAdopters() public view returns (address[16]) {
        return adopters;
    }
}

주로 스마트 계약을 테스팅시 자바스크립트(mocha)로 계약 함수를 신뢰성을 테스트한다. 하지만 여기에선 바로 솔리디티 언어로 테스팅하는 걸 보여준다.

pragma solidity ^0.4.17;

//다양한 테스팅 도구 제공
import "truffle/Assert.sol";
//이미 서버에 배포된 계약 주소를 가져온다. 
import "truffle/DeployedAddresses.sol";
//계약 소스를 가져온다. 
import "../contracts/Adoption.sol";

contract TestAdoption {
    //Adoption 초기화
    Adoption adoption = Adoption(DeployedAddresses.Adoption());

    //Pet에 Adopt를 할수 있는지 테스트
    function testUserCanAdoptPet() public {
        //8번 아이디를 adopt 함수 실행
        uint returnedId = adoption.adopt(8);

        uint expected = 8;


        //값이 제대로 가져오는 지 체크
        Assert.equal(returnedId, expected, "Adoption of pet ID 8 should be recoreded");
    }

    //아이디를 통해 Adopter 주소를 가져오는 걸 테스팅
    function testGetAdopterAddressByPetId() public {
        //트랙잭션을 할 예정이므로 예상 값을 this로 설정 가능하다. (펫 구매한 사람 주소)
        address expected = this;

        //adopters 변수 설정되면 public getter 가 되므로 거기 위치에 8의 값을 가져와서 테스팅해본다. 
        address adopter = adoption.adopters(8);

        //값 맞게 들어오는지 체크
        Assert.equal(adopter, expected, "Owner of pet Id 8 should be recored");
    }

    function testGetAdopterAddressByPetIdInArray() public {
        //트랙잭션을 할 예정이므로 예상 값을 this로 설정 가능하다. (펫 구매한 사람 주소)
        address expected = this;

        //storeage, memory 를 설정해서 실제 물리적으로 저장 할 것인지 내부에서 저장할 건지 정한다. 
        //adoption 클래스에서 adopter 배열목록을 가져온다. 
        address[16] memory adopters = adoption.getAdopters();

        //가져온 adopter의 8번째가 맞는지 체크
        Assert.equal(adopters[8], expected, "Owner of pet ID 8 should be recorded.");
    }
}

그리고 다음과 같이 테스팅을 해본다.

> truffle test
or
devlope 모드일 경우 단지 test 만 입력
Using network 'development'.

   Compiling ./contracts/Adoption.sol...
   Compiling ./test/TestAdoption.sol...
   Compiling truffle/Assert.sol...
   Compiling truffle/DeployedAddresses.sol...

     TestAdoption
       ✓ testUserCanAdoptPet (91ms)
       ✓ testGetAdopterAddressByPetId (70ms)
       ✓ testGetAdopterAddressByPetIdInArray (89ms)


     3 passing (670


그리고 웹쪽 프론트에서 이 계약 함수들을 사용하는 내용이다. App.js 에서 볼 수 있다.

주석을 달아봄으로써 해당 기능을 알아볼 예정이다. 

App = {
  web3Provider: null,
  contracts: {},

  //초기화
  init: function() {
    //펫 배열을 가져와서 리스트를 만든다. 
    $.getJSON("../pets.json", function(data) {
      var petsRow = $("#petsRow");
      var petTemplate = $("#petTemplate");

      for (i = 0; i < data.length; i++) {
        petTemplate.find(".panel-title").text(data[i].name);
        petTemplate.find("img").attr("src", data[i].picture);
        petTemplate.find(".pet-breed").text(data[i].breed);
        petTemplate.find(".pet-age").text(data[i].age);
        petTemplate.find(".pet-location").text(data[i].location);
        petTemplate.find(".btn-adopt").attr("data-id", data[i].id);

        petsRow.append(petTemplate.html());
      }
    });
    
    //지갑설정 함수를 실행한다. 
    return App.initWeb3();
  },

  
  initWeb3: function() {
    // Is there an injected web3 instance?
    if (typeof web3 !== "undefined") {
      //Metamask 가 실행시 현 지갑을 리턴한다. 
      App.web3Provider = web3.currentProvider;
    } else {
      //만약 지정된 지갑이 없는 경우 미리 설정된 Ganeche 지갑을 리턴한다. 
      App.web3Provider = new Web3.providers.HttpProvider(
        "http://localhost:7545"
      );
    }
    web3 = new Web3(App.web3Provider);

    return App.initContract();
  },
  
  //계약 초기화
  initContract: function() {
    /*
    Adoption.JSON 형태 
    {
    "contractName": "Adoption",
    "abi": [
      {
        "constant": true,
        "inputs": [
          {
            "name": "",
            "type": "uint256"
          }
        ],
        "name": "adopters",
       ...............
    */
    //Adoption 은 컴파일 시 나온 ABI JSON이다 여기에 기본 함수들이 표시가 된다. 
    //웹에서는 이 ABI JSON을 보고 실행을 할 수 있다.
    $.getJSON("Adoption.json", function(data) {
      // Get the necessary contract artifact file and instantiate it with truffle-contract
      var AdoptionArtifact = data;
      //미리 제공된 truffleContract 를 통해 Adoption 인스턴스 생성
      App.contracts.Adoption = TruffleContract(AdoptionArtifact);
      
      //지갑 설정
      App.contracts.Adoption.setProvider(App.web3Provider);

      //
      // 이미 채택된 애완 동물이 있는 경우 함수를 호출해서 데이터 확인 후 업데이트 할수 있도록 한다. 
      return App.markAdopted();
    });

    return App.bindEvents();
  },

  //이벤트 바인딩
  bindEvents: function() {
    $(document).on("click", ".btn-adopt", App.handleAdopt);
  },

  //채택된 아이템이 있는 경우 버튼을 success 로 변경
  markAdopted: function(adopters, account) {
    var adoptionInstance;

    App.contracts.Adoption.deployed()
      .then(function(instance) {
        adoptionInstance = instance;
        
        //전체 adopte 배열 주소를 리턴한다. 
        return adoptionInstance.getAdopters.call();
      })
      .then(function(adopters) {
        //for 문 돌면서 adopte 만큼 버튼을 success 로 바꾸고 비활성화를 시킨다. 
        for (i = 0; i < adopters.length; i++) {
          if (adopters[i] !== "0x0000000000000000000000000000000000000000") {
            $(".panel-pet")
              .eq(i)
              .find("button")
              .text("Success")
              .attr("disabled", true);
          }
        }
      })
      .catch(function(err) {
        console.log(err.message);
      });
  },
  //Adopt 버튼 클릭시 adopt 함수 실행하는 함수이다. (트랙잭션이 발생된다.)
  handleAdopt: function(event) {
    //기본 이벤트 블럭함
    event.preventDefault();

    //펫 아이디를 id 를 통해 쿼리해옴
    var petId = parseInt($(event.target).data("id"));

    var adoptionInstance;
    
    //지갑상에 주소를 가져온다. 
    web3.eth.getAccounts(function(error, accounts) {
      if (error) {
        console.log(error);
      }

      //처음 주소를 가져온다. 
      var account = accounts[0];

      
      App.contracts.Adoption.deployed()
        .then(function(instance) {
          adoptionInstance = instance;
          
          
          //petId, account를 넣어서 adopt 함수를 실행한다. 
          return adoptionInstance.adopt(petId, { from: account });
        })
        .then(function(result) {
        //완료 된 후 success 버튼으로 변경...잘 안됨..
          return App.markAdopted();
        })
        .catch(function(err) {
          console.log(err.message);
        });
    });
  }
};

$(function() {
  $(window).load(function() {
  //초기화
    App.init();
  });
});

Truffle을 이용해서 손쉽게 테스팅 가능하며 배포도 쉽게 가능한 걸 볼수 있다. 잘 배워놓으면 도움이 많이 된다고 본다. 


Truffle기초 - 계약 디버깅과 테스팅

광고 클릭시 큰 힘이 됩니다. 감사합니다. 

Truffle디버깅 및 테스팅 하는 방법을 알아보자.

Truffle 은 DAPP 개발을 편하게 해주는 프레임워크이다. 테스팅 및 컴파일, 배포까지 쉽게 해준다.

테스팅은 Ganache 를 이용한다. Ganache 은 개발모드에서 가상으로 테스팅 및 배포까지 하게 해주는 프레임워크

다른 웹에서 쉽게 테스팅을 할시 http://remix.ethereum.org/ 에서 쉽게 가능하다.

그리고 truffle에서도 지원한다.

우선 작업 할 폴더를 구성하자.

mkdif simple-contract
cd simple-contract

truffle을 초기화를 해서 기본 구성을 만들 수 있다.

truffle init

기본 구성은 위와 같이 나온다.

  • build: 컴파일시 나오는 스마트 계약 빌드 파일이다. ABI파일이 나오는 걸로 보인다.
  • contracts: 스마트 계약 파일들을 여기에서 보관한다.
  • migrations: 이미 배폰 계약에 대해서 버전링을 하는 것이다. 자세한 내용은 여기를 참고하기 바란다.
  • test: 계약을 테스팅 할 수 있다.

위에보이는 truffle.js 등은 서버설정 및 기타 설정이 가능하다.

초기화를 진행 한 후 스마트 계약(.sol) 을 만들어 보자.

여기에서는 visual stuio code 를 사용할 것이다.

플러그인으로는 solidity설치해서 재 시작한다.

pragma solidity ^0.4.17;

contract SimpleStorage {
    uint myVariable;

    function set(uint x) public {
        myVariable = x;
    }

    function get() constant public returns (uint) {
        return myVariable;
    }
}

두가지 간단한 함수들이 있다. Set, Get 등이다. 변수를 넣고 가져오는 계약이다.

이제 /migrations폴더에 배포함수를 넣어보자. 이름은 2_deploy_contracts.js 으로 하자.

var SimpleStorage = artifacts.require("SimpleStorage");

module.exports = function(deployer) {
  deployer.deploy(SimpleStorage);
};

그리고 컴파일을 해본다.

> truffle compile
...............
Compiling .\contracts\Store.sol...
Writing artifacts to .\build\contracts

컴파일 후에 build 폴더에 계약 관련 파일이 생성되었다.

실서버에 배포를 하기 전에 제대로 솔리디티에서 함수들의 신뢰성들을 테스팅 할 필요가 있다. Truffle 프레임워크 단에서 편하게 지원해주니 우린 잘 쓰면 된다.

그럼 개발 모드로 들어가본다.

> truffle develop
Connected to existing Truffle Develop session at http://127.0.0.1:9545/
truffle(develop)> //입장

개발 모드로 실행 및 세팅된 상태이다.

그럼 배포를 해보자.

migrate

배포를 한 후 결과값을 보면 다음과 같다.

  Replacing Migrations...
  ... 0x620c6bad3d8c76712a1262667b7944d125a882ac485f7ff73bb20af13b882035
  Migrations: 0x8acee021a27779d8e98b9650722676b850b25e11
Saving successful migration to network...
  ... 0x5ef83bc84690e0a14a30bb02f358bc1ff44f19ab487996f7323f063e95e417f4
Saving artifacts...
Running migration: 2_deploy_contracts.js
  Deploying SimpleStorage...
  ... 0x79e9162e22395f000597903ac21f461f22240d4ccd22f51f2bcfde726147fb14
  SimpleStorage: 0x4e71920b7330515faf5ea0c690f1ad06a85fb60c
Saving successful migration to network...
  ... 0xa56b2ca521875b0ee0e9ab32d12ce376b0f56930043f4ae55674a0c04510f2ec
Saving artifacts...

그러면 테스팅 서버에 배포가 된 걸로 보인다.

이제 테스팅 코드를 작성해서 실제 값이 잘 넣고 가져오는 지 테스트를 해보자.

/test 폴더내 testing_simplestorage.js 를 만들자.

그리고 내용에 다음과 같이 자바스크립트로 테스팅을 해본다.

테스팅시 Promise 사용하는 방법과 Await 문법을 각각 사용해서 테스팅 해보았다.

자바스크립트 테스팅 관련 자세한 문서 바로가기

var SimpleStorage = artifacts.require("SimpleStorage");

contract("SimpleStorage", function(accounts) {
  it("should value is 0", function() {
    SimpleStorage.deployed()
      .then(function(instance) {
        return instance.get.call();
      })
      .then(function(value) {
        assert.equal(value, 0, "value is 0");
      })
      .catch(e => {
        console.log(e);
      });
  });
  it("should value is 4 when i put the 4 about value", async () => {
    try {
      let instance = await SimpleStorage.deployed();
      await instance.set(4); //4를 넣어본다.

      let instance2 = await SimpleStorage.deployed();
      let value = await instance2.get.call();
      assert.equal(value, 4, "value is 4"); // 그리고 결과값이 4로 나오는 지 확인해본다. 
    } catch (e) {
      console.log(e);
    }
  });
});

콘솔에서 바로 테스팅도 가능하다.

테스팅전에 배포환경을 초기를 해야한다.

> migrate --reset
> test
> truffle develope
SimpleStorage.deployed().then(function(instance){return instance.get.call();}).then(function(value){return value.toNumber()});
> 0
SimpleStorage.deployed().then(function(instance){return instance.set(4);});

SimpleStorage.deployed().then(function(instance){return instance.get.call();}).then(function(value){return value.toNumber()});
> 4 //4로 나오는 걸 확인

자바스크립와 콘솔에서 테스팅시 차이점은 트랙잭션이 일어날시 로그가 좀 다르게 나온다.

// 트렉잭션 발생시켜본다.
SimpleStorage.deployed().then(function(instance){return instance.set(4);});
............................ 결과 출력
{ tx: '0x8a7d3343dd2aaa0438157faae678ca57cc6485825bb4ed2ebefe90609dd268ce',
  receipt:
   { transactionHash: '0x8a7d3343dd2aaa0438157faae678ca57cc6485825bb4ed2ebefe90609dd268ce',
     transactionIndex: 0,
     blockHash: '0xd66fc7ddf829f1d1ee4a655c0b6a7de537748865de39de28b2167d8485fd9e92',
     blockNumber: 5,
     gasUsed: 41642,
     cumulativeGasUsed: 41642,
     contractAddress: null,
     logs: [],
     status: '0x01',
     logsBloom: '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000' },
  logs: [] }

출력값을 사용자별로 다르게 나올수 있다.

위와 같은 내용은 정상적으로 계약이 이루어 질 경우이다.

디버깅

솔리디티(.sol) 에서 스마트 계약 개발시 컴파일은 되지만 런타임에서 디버깅을 해서 잘 동작되는지 체크 해볼 필요가 있다.

우선 솔리디티에서 강제적으로 오류를 발생시켜 보자.

계약 내 set 함수를 아래와 같이 바꾼 후 다시 배포를 해보자.

function set(uint x) public {
    assert(x == 0); //0일 경우에만 통과를 하도록 한다. 
    myVariable = x;
}

다시 배포를 해보자.

> migrate --reset

디버깅시 실행시 개발모드에서 로그도 출력을 볼 수 있다.

그러기 위해선 Terminal 을 따로 열어서 확인을 할 수 있다.

+을 클릭해서 터미널을 추가한다.

그리고 개발에서 로그모드로 다시 접속한다.

> truffle develop --log
Connected to existing Truffle Develop session at http://127.0.0.1:9545/

그리고 처음 콘솔로 가서 다시 트렉젝션을 실행 시켜본다.

truffle(develop)> migrate --reset // 배포 초기화
Compiling .\contracts\Store.sol...
Writing artifacts to .\build\contracts

Using network 'develop'.

Running migration: 1_initial_migration.js
  Replacing Migrations...
  ... 0xeab337bfe489629610d34f11044e77fbfc37cfbea4b614f2e12c87ec45ea6f35
  Migrations: 0x2c2b9c9a4a25e24b174f26114e8926a9f2128fe4
Saving successful migration to network...
  ... 0x9b51540f5a7d75a8fc920e3e5e4ec66792ba31fd006bd176901f0e6347af2dba
Saving artifacts...
Running migration: 2_deploy_contracts.js
  Replacing SimpleStorage...
  ... 0x6622a3183323a916e08b9e1d55ec0f848ca75174adae954ebe796e0e66d0ae3f
  SimpleStorage: 0xfb88de099e13c3ed21f80a7a1e49f8caecf10df6
Saving successful migration to network...
  ... 0x69eaa7ed49cc72426706d54c4f52ba70b742ed6910f1223eb0df5f250b4b8ec3
Saving artifacts...
//트렉잭션 실행하기
truffle(develop)> SimpleStorage.deployed().then(function(instance){return instance.set(4);});

실행시 오류내용을 확인할 수 있다.

Error: VM Exception while processing transaction: invalid opcode //0이 아니므로 오류 발생
    at XMLHttpRequest._onHttpResponseEnd (C:\Users\tommy\AppData\Roaming\npm\node_modules\truffle\build\webpack:\~\xhr2\lib\xhr2.js:509:1)
    at XMLHttpRequest._setReadyState (C:\Users\tommy\AppData\Roaming\npm\node_modules\truffle\build\webpack:\~\xhr2\lib\xhr2.js:354:1)
    at XMLHttpRequestEventTarget.dispatchEvent (C:\Users\tommy\AppData\Roaming\npm\node_modules\truffle\build\webpack:\~\xhr2\lib\xhr2.js:64:1)
    at XMLHttpRequest.request.onreadystatechange (C:\Users\tommy\AppData\Roaming\npm\node_modules\truffle\build\webpack:\~\web3\lib\web3\httpprovider.js:128:1)
    at C:\Users\tommy\AppData\Roaming\npm\node_modules\truffle\build\webpack:\~\truffle-provider\wrapper.js:134:1
    at C:\Users\tommy\AppData\Roaming\npm\node_modules\truffle\build\webpack:\~\web3\lib\web3\requestmanager.js:86:1
    at Object.InvalidResponse (C:\Users\tommy\AppData\Roaming\npm\node_modules\truffle\build\webpack:\~\web3\lib\web3\errors.js:38:1)

그리고 다시 2번째 콘솔로 가서 로그를 확인해본다.

Connected to existing Truffle Develop session at http://127.0.0.1:9545/

  ..............

  develop:testrpc eth_sendTransaction +2s
  develop:testrpc  +17ms
  develop:testrpc   Transaction: 0x2cc0d39fc0bec51835df91343e64577b34ae335f7d998143349d5ab8b3d63181 +1ms //이부분이 중요하다. 
  develop:testrpc   Gas usage: 6721975 +0ms
  develop:testrpc   Block Number: 11 +0ms
  develop:testrpc   Block Time: Sat Mar 31 2018 09:56:57 GMT+0900 (대한민국 표준시) +1ms
  develop:testrpc   Runtime Error: invalid opcode +0ms
  develop:testrpc  +1ms
Transaction: 0x2cc0d39fc0bec51835df91343e64577b34ae335f7d998143349d5ab8b3d63181

트렉잭션 아이디를 이용해서 디버깅이 가능하다. 첫번째 콘솔로 가서 아래와 같이 디버깅을 실행해본다.

debug 0x2cc0d39fc0bec51835df91343e64577b34ae335f7d998143349d5ab8b3d63181
Gathering transaction data...

Addresses affected:
 0xfb88de099e13c3ed21f80a7a1e49f8caecf10df6 - SimpleStorage

Commands:
(enter) last command entered (step next)
(o) step over, (i) step into, (u) step out, (n) step next
(;) step instruction, (p) print instruction, (h) print this help, (q) quit
(b) toggle breakpoint, (c) continue until breakpoint
(+) add watch expression (`+:<expr>`), (-) remove watch expression (-:<expr>)
(?) list existing watch expressions
(v) print variables and values, (:) evaluate expression - see `v`


Store.sol:

1: pragma solidity ^0.4.17;
2:
3: contract SimpleStorage {
   ^^^^^^^^^^^^^^^^^^^^^^^^

debug(develop:0x2cc0d39f...)>

Store.sol:

4:     uint myVariable;
5:
6:     function set(uint x) public {
       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

debug(develop:0x2cc0d39f...)>

Store.sol:

5:
6:     function set(uint x) public {
7:         assert(x == 0); //0일 경우에만 통과를 하도록 한다.
                       ^

debug(develop:0x2cc0d39f...)>

Store.sol:

5:
6:     function set(uint x) public {
7:         assert(x == 0); //0일 경우에만 통과를 하도록 한다.
                  ^

debug(develop:0x2cc0d39f...)>

Store.sol:

5:
6:     function set(uint x) public {
7:         assert(x == 0); //0일 경우에만 통과를 하도록 한다.
                  ^^^^^^

debug(develop:0x2cc0d39f...)>

Store.sol:

5:
6:     function set(uint x) public {
7:         assert(x == 0); //0일 경우에만 통과를 하도록 한다.
           ^^^^^^^^^^^^^^

엔터를 누르면 차례로 진행되면서 오류가 발생되는 부분에 표시가 된다. 이런식으로 단계별로 디버깅을 할 수 있다.

Remix 를 통해서 디버깅이 쉽게 되는 점도 알아놓으면 도움이 많이 된다.

이 외에에도 다양한 케이스에서 테스팅가능하다.

소스 링크

참조 링크


광고 클릭시 많은 힘이 됩니다. 감사합니다. 




혹시 크립토키티 라는 걸 듣어보셨나요?  이더리움상에서 DAPP 형태로 고양이를 랜덤으로 만들어서 거래까지 가능한 플랫폼이다.

현재 매출은 1200억 달러를 넘어섰다고 한다. 

관련 기사 모음

이런 플랫폼 게임 개발시 필요한 DAPP 개발 지식을 무료로 튜터리얼식으로 만들어서 공유 하는 곳이 있다.

크립토좀비 바로가기

왼쪽에는 설명이 나오고 오른쪽은 코드 실습을 통해서 하나씩 배워나가는 방식이다.

개발 내용은 DAPP 개발시 필요한 솔리티디 언어로 진행한다. 

레벨이 현재 5개로 나누어 (계속 추가될 예정) 지며 각각의 레벨과 함께 게임도 같이 만들어 나가는 재미도 있다. 

이 글은 여기 튜터리얼에 나온 레벨별로 요약을 해보았다. 종종 훝어 보기 편하게..^^

레벨 1

배열

배열에는 정적배열과 동적배열 두 종류가 있다.

unit[2] fixedArray; //2개의 원소를 담을 수 있는 고정길이 배열

string[5] stringArray; //또다른 고정 배열으로 5개의 스트링을 담을 수 있다. 

unit[] dynamicArray; //동적배열은 고정된 크기가 없으며 계속 크기가 커질 수 있다.

구조체 배열을 생성할 수 있다.

Person[] people: //이는 동적 배열로 원소를 계속 추가 가능함

Public으로 배열을 선언 가능하다. 이 경우는 자동으로 getter함수가 추가된다.

Person[] public people //public이 중간에 붙는 걸 유의하자

배열에 추가를 하는 경우

Person satoshi = Person(172, "Satoshi");
people.push(satoshi) //이런식으로 추가 가능하다.

함수 선언은 다음과 같다.

function eat(string _name, uint _amount) {
    //TODO
}

eat('hambuger',1000); //함수의 실행

private으로도 생성을 할 수 있다.

function addToArray(uint _number) private {
  numbers.push(_number)
}

반환값 반환하는 경우

string greeting = "what's up dog"

function sayHello() public returns (string) { //string 타입으로 리턴합니다. 
  return greeting;
}

함수제어자의 경우 view와 pure를 가지고 있다.

view의 경우 함수가 데이터를 보기만 하고 변경하지 않는 다는 뜻이다.

pure는 어떤 데이터도 접근하지 않는 것을 의미한다. 앱에서 읽지도 않고, 반환값이 함수에 전달된 인자값에 따라서 달라진다.

솔리디티 컴파일러는 이러한 부분에 대해서 무엇을 쓰면 좋을지 경고를 표시해준다.

function sayHello() public view returns (string) {

}

function _multiply(uint a, uint b) private pure returns (uint) {
  return a * b;
}

랜덤문자 생성

솔리디티에서 랜덤 발생하는 방법은 SHA3의 한 종류인 keccak256을 이용하면 된다.

완벽하고 안전한 난수 발생기는 아닌걸 유의하자.

//6e91ec6b618bb462a4a6ee5aa2cb0e9cf30f7a052bb467b0ba58b8748c00d2e5
keccak256("aaaab");
//b1f078126895a1424524de5321b339ab00408010b7cf0e6ed451514981e58aa9
keccak256("aaaac");

위 글자 하나때문에 값이 완전히 달라지는 걸 유념하자.

형 변환

가끔씩 형병환 할 경우가 있다.

uint a = 5;
uint b = 6;
// a * b가 uint8이 아닌 uint를 반환하기 때문에 에러메세지가 나옴
uint8 c = a * b
// b를 unit8 으로 형변환해서 코드가 제대로 동작하기 위해서...
uint8 c = a * uint8(b);

이벤트

이벤트는 계약이 블록체인 상에서 어떠한 이벤트 발생시 의사소통 하는 방법을 말한다.

계약은 특정이벤트가 발생되는지 리스닝하는 걸 뜻한다.

event IntegersAdded(uint x, uint y, uint result);

function add(uint _x, uint _y) public {
    uint result = _x + _y;
    //이벤트를 실행하여 앱에게 add 함수가 실행되었음을 알린다. 
    IntegersAdded(_x, _y, result);
    return result;
}

//////////////// 호출은
MyContract.IntegerAdded(function(error,result){
    //결과와 관련된 행동을 취한다.     

});


레벨2

Address

이더리움 블록체인은 은행 계좌와 같은 계정들로 이루어져있음

예를 들어

0x0cE446255506E92DF41614C46F1d6df9Cc969183

이런 주소들을 이용해서 상호간에 이더등을 주고 받을수 있다.

주소는 특정 유저 ( 또는 스마트 컨트렉트)가 소유한다

Mapping

(key, Value) 형식으로 되어 있음

mapping (address => uint) public accountBalance; //유저의 계좌 잔액을 보유 하는 uint 지정가능

mapping (uint => string) userIdToName; //key는 uint 이고 값은 string 이다.

Msg.sender

솔리디티에서 함수 실행은 항상 호출을 해야지 실행이 되는 구조를 가지고 있다. 그래서 누군가가 항상 있다는 게 성립된다.

그래서 실행하는 계정이 msg.sender 로 정의된다.

위에 배운 mapping을 이용해서 msg.sender를 어떻게 이용하는지 살펴보자.

mapping (address => uint) favoriteNumber;

function setMyNumber(uint _myNumber) public {
  favoriteNumber[msg.sender] = _myNumber;
}

function getMyNumber() public view returns (uint) {
  return favoriteNumber[msg.sender];
}

Require

특정 조건이 아닐 경우 에러 메시지를 발생하고 실행을 멈추는 기능을 말한다.

function checkName(string _name) public returns (string) {
    require(keccak256(_name) == keccak256("tommy"));

    return "toooooomy!!";
}

상속

하나의 긴 스마트 계약보다는 나누어서 여러개의 스마트 계약으로 만들 수 있다.

아래 계약은 Animal이 있는 함수를 상속받고 있는 Dog가 함수를 그대로 사용하고 있다는 것이다.

contract Animal {
    function eat() public returns (string) {
        return "eat";
    }
}

contract Dog is Animal {
    function eatAll() public returns (string) {
        return eat();
    }
}

Import

여러개의 솔리디티 파일(.sol) 을 가져와서 사용할 수 있다.

import './testContract1.sol';

contract testContract2 is testContract1 {

}

Storage vs Memory

Storage는 블록체인 상에서 영구적으로 저장 하는 변수이다. (전역 변수)

Memory는 함수내에(메모리) 임시로 저장되는 변수.

솔리디티는 함수 외부에 쓰인 변수에는 자동으로 storage로 만들어 준다.

사용하는 경우는 전에 배웠던 구조체와 배열에서 사용된다.

contract SandwichFactory {
    struct Sandwich {
        string name;
        string status;
    }

    Sandwich[] sandwiches;

    function eatSandwich(uint _index) public {


        //Sandwich mySandwich = sandwiches[_index];
        //스토리지 변수에 넣으라는 경고 메세지를 볼수 있다(아래 그림 참고)

        Sandwich storage mySandwich = sandwiches[_index];
        //스토리지에 넣게 된다. 

        mySandwich.status = "Eaten!";
        //Save to storage sandwiches array items

        mySandwich.status = "Eaten!";
        //스토리즈로 저장됨

        Sandwich memory anotherSandwich = sandwiches[_index + 1];
        //하지만 복사객체를 만들어서 메모리상으로 만듬

        anotherSandwich.status = "Eaten!";
        //메모리상으로 복사객체에 넣게 되서 sandwiches 배열과는 무관함

        sandwiches[_index + 1] = anotherSandwich;
        //다시 스토리지로 저장된다. 

    }
}

접근 제어자

솔리디티는 publicprivateinternalexternal등 4가지의 제어자를 제공한다.

internal의 경우 함수가 정의된 컨트렉트를 상소하는 컨트렉트에서 접근 가능

external은 함수가 컨트렉트 밖에서만 호출될 수 있다. 즉 같은 컨트렉트안에서 해당 함수를 호출을 제한한다.

internal와 private의 경우 성격이 비슷하며

external은 public 과 성격이 비슷하다고 볼 수 있다.

다른 컨트렉트와 상호작용

이미 올라가져 있는 컨트렉트를 인터페이스로 가져와서 사용도 가능하다.

pragma solidity ^0.4.17;

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];
    }
}

위와 같은 계약이 이미 올라가 있다고 가정해보자.

그럼 인터페이스를 만들고

contract NumberInterface {
    //위 계약의 getNum 를 호출한다. 
    function getNum(address _myAddress) public view returns (uint); //껍데기만 정의한다. 
}

이 인터페이스를 선언하고 다른 컨트렉트에서 사용하면 된다.

contract MyContract {
    address addr = 0xabcdef..... //이더상의 계약 주소

    NumberInterface numberContract = NumberInterface(addr);
    //생성한다.

     function someFunction() public {
         //이제 getNum을 호출한다. 
         uint num = numberContract.getNum(msg.sender);
         //이제 LuckyNumber 상의 getNum을 호출해서 사용할 수 있게 한다. 
     }     
}

다수의 반환값 처리하기

솔리디티에서는 리턴값을 여러개를 가질 수 있다.

function multipleReturns() internal returns(uint a, uint b, uint c) {
    return (1, 2, 3);
}

function processMultiReturns() external {
    uint a;
    uint b;
    uint c;
    //다수값을 설정함    
    (a, b, c) = multipleReturns();
}

function getLastReturnValue() external {
    uint c;
    //비어있는 건 빈값으로 설정가능
    (,,c) = multipleReturns();
}

IF 문

솔리디티에서 If문은 자바스크립트의 if문과 동일하다.

function eatBLT(string sandwich) public {
    //스트링(문자)간의 동일 여부를 판단하기 위해선 keccak256 을 사용해서 해시값 비교를 해야한다는 점 유의
    if(keccak(sandwich) == keccak256("BLT")){
        eat();
    }
}


레벨3

OpenZepplin의 Ownable 컨트렉트

계약에 대해 소유권을적용할 수 있는 라이버러리이다.

소스를 잠깐 살펴보면 다음과 같다.

pragma solidity ^0.4.17;

contract Ownable {
    address public owner;
    event OwnerShipTransferred(address indexed previousOwner, address indexed newOwner);

    function Ownable() public {
        owner = msg.sender;
    }

    modifier onlyOwner() {
        require(msg.sender == owner);
        _;
    }

    function transferOwnership(address newOwner) public onlyOwner {
        require(newOwner != address(0));
        OwnerShipTransferred(owner, newOwner);
        owner = newOwner;
    }
}

위 코드엔 생성자 , 함수 제어자 (modify onlyOwner())( require를 확장해서 사용가능하다. )

_; 는 owner인걸 확인 한 후에 그 다음 코드를 포함한다는 뜻(실행함)

즉 위 코드는 Ownable 컨트렉트 생성시 msg.sender (컨트렉트를 배포한 사람) 을 대입한다.

그리고 onlyOwner 라는 접근 제어자가 붙은 경우 사용할 수 있게끔 접근 제어자를 붙인다.

그리고 새로운 소유자에게 이동할수 있게끔 하는 함수도 있다. ( transferOwnership)

일반적인 dApp에서 자주 쓰는 것이므로 확장해서 사용하는 편이 좋다.

함수 제어자

전에 require를 통해서 제어를 했었다.

이젠 함수 자체를 제어하는 방법을 배워보자.

modifier를 함수에 붙여 주면 그 함수를 제어 할 수 있다.

.. Ownable 계약서에 있는
modifier onlyOwner() {
    require(msg.sender == owner);
    _
}

contract MyContract is Ownable {
    event eat(string food);

    function eatBread() external onlyOwner {
        eat("Bread");
    }
}

가스 - 이더리움 DApp이 사용하는 연료 {#-dapp-}

솔리디티에서는 사용자들이 자네가 만든 DApp의 함수를 실행할 때마다_가스_라고 불리는 화폐를 지불해야 하네. 사용자는 이더(ETH, 이더리움의 화폐)를 이용해서 가스를 사기 때문에, 자네의 DApp 함수를 실행하려면 사용자들은 ETH를 소모해야만 하네.

함수를 실행하는 데에 얼마나 많은 가스가 필요한지는 그 함수의 로직(논리 구조)이 얼마나 복잡한지에 따라 달라지네. 각각의 연산은 소모되는_가스 비용(gas cost)이 있고, 그 연산을 수행하는 데에 소모되는 컴퓨팅 자원의 양이 이 비용을 결정하네. 예를 들어, storage에 값을 쓰는 것은 두 개의 정수를 더하는 것보다 훨씬 비용이 높네. 자네 함수의 전체가스 비용_은 그 함수를 구성하는 개별 연산들의 가스 비용을 모두 합친 것과 같네.

함수를 실행하는 것은 자네의 사용자들에게 실제 돈을 쓰게 하기 때문에, 이더리움에서 코드 최적화는 다른 프로그래밍 언더들에 비해 훨씬 더 중요하네. 만약 자네의 코드가 엉망이라면, 사용자들은 자네의 함수를 실행하기 위해 일종의 할증료를 더 내야 할 걸세. 그리고 수천 명의 사용자가 이런 불필요한 비용을 낸다면 할증료가 수십 억 원까지 쌓일 수 있지.

가스는 왜 필요한가? {#-}

이더리움은 크고 느린, 하지만 굉장히 안전한 컴퓨터와 같다고 할 수 있네. 자네가 어떤 함수를 실행할 때, 네트워크상의 모든 개별 노드가 함수의 출력값을 검증하기 위해 그 함수를 실행해야 하지. 모든 함수의 실행을 검증하는 수천 개의 노드가 바로 이더리움을 분산화하고, 데이터를 보존하며 누군가 검열할 수 없도록 하는 요소이지.

이더리움을 만든 사람들은 누군가가 무한 반복문을 써서 네트워크를 방해하거나, 자원 소모가 큰 연산을 써서 네트워크 자원을 모두 사용하지 못하도록 만들길 원했다네. 그래서 그들은 연산 처리에 비용이 들도록 만들었고, 사용자들은 저장 공간 뿐만 아니라 연산 사용 시간에 따라서도 비용을 지불해야 한다네.

참고: 사이드체인에서는 반드시 이렇지는 않다네. 크립토좀비를 만든 사람들이 Loom Network에서 만들고 있는 것들이 좋은 예시가 되겠군. 이더리움 메인넷에서 월드 오브 워크래프트 같은 게임을 직접적으로 돌리는 것은 절대 말이 되지 않을 걸세. 가스 비용이 엄청나게 높을 것이기 때문이지. 하지만 다른 합의 알고리즘을 가진 사이드체인에서는 가능할 수 있지. 우린 다음에 나올 레슨에서 DApp을 사이드체인에 올릴지, 이더리움 메인넷에 올릴지 판단하는 방법들에 대해 더 얘기할 걸세.

uInt 팁

솔리디티 안에서 uint8, uint16, uint32 등의 타입을 지정하는 건 별로 의미가 없다. 이유는 생성때부터 256비트의 저장공간을 잡아놓기 때문이다. 즉 uint256 == uint8 과 같은 공간을 쓴다는 뜻이다.

하지만 예외 상황이 있다. 바로 struct안에서이다.

struct Normal {

uint a;

}

struct Mini {

uint32 a;

}

Normal normal = Normal(10);

Mini mini = Mini(10);

//이 2개는 같지만 가스를 소모하는 게 다르다.

struct Mini {

uint32 a;

uint32 b;

uint c;

}

struct Mini {

uint32 a;

uint c;  

uint32 b;  

}

//이 2개도 같아 보이지만 옆으로 바로 선언하는게(상위) 가스를 덜 소모한다.

시간 단위(Time uints)

솔리디티는 유닉스 타임을 쓰고 있다. now를 사용시 현재의 유닉스 타임스탬프를 값을 가지고 올 수 있다.

솔리디티는 또한 secondsminuteshoursdaysweeksyears

을 제공하고 있다.

1 minutes = 60
1 hours = 3600 (60 * 60)
1 days = 86400 ( 24 * 1 hours)

그럼 사용예제를 보자.

uint lastUpdated;

function updateTimestamp() public {
    lastUpdated = now //now로 설정
}
//5분이 지난 경우 true , 아닌 경우 false
function fiveMinutesHavePassed() public view returns(bool) {
    return (now >= (lastUpdated + 5 minutes));
}

캐스팅

uint 256 를 uint32 으로 캐스팅시

uint32(uint256 변수) //이런식으로 캐스팅함

구조체를 인수로 전달하기

storage 구조체를 주고 받을 수 있다.

function _doStuff(Zombie storage _zombie) internal {
    // _zombie 처리
}

Public 함수 보안

함수를 public 으로 지정시 외부에서 인터페이스로 해당 함수를 호출할 수 있다는 걸 유의해야 한다.

그래서 위에 언급했던 onlyOwner를 사용을 할 수 있고 그 외 private또는 internal로 만들 수 있다.

인수를 가지는 함수 제어자

// 사용자의 나이를 저장하는 매핑
mapping ( uint => uint ) public age;

//사용자의 특정 나이 이상인지 확인 하는 제어자
modifier olderThan( uint _age, uint _userId ) {
    require(age[_userId]) >= _age);
    _;
}

function buyAlchoDrinks(uint _userId) public orderThan(16, _userId) {
    //필요한 함수 내용들
}

View

함수가 데이터를 쓰고,수정,삭제를 하지 않고 오직 읽기만 할 경우 사용될 수 있다.

View 함수는 사용자에 의해서 외부에서 호출 시 가스를 전혀 소모하지 않는다.

즉 로컬 노드에 데이터만 조회하고 따로 어떠한 트렉젝션이 필요없다는 뜻이다.

external view //오직 읽기에만 사용가능하다. 

Storage는 비싸다.

데이터의 일부를 쓰거나 바꿀때마다 블록체인에 영구적으로 기록되기 때문에 가스비가 들수 밖에 없다.

비용이 드는 문제이다.

그래서 비용을 최소화 하기 위해 필요한 경우가 아니면 storage에 데이터를 쓰지 않는 편이 좋다.

메모리에 배열 선언하기

Storage 에 쓰지 않고 배열을 만들려면 memory 키워드를 이용하면 된다.

Storage 보다 휠씬 가스비가 절약할 수 있다. 외부에서 호출하는 View 함수라는 무료이다.

function getArray() external pure returns(uint[]){
    //배열의 크기는 항상 주어져야 한다. 이후에는 변경될 수 있을것이다. 
    uint[] memory values = new uint[](3);
        
    values.push(1);
    values.push(2);
    values.push(3);
    
    return values;
}

For 반복문 사용하기

솔리디티에서 for문은 자바스크립트와 유사하다.

function getEvens() pure external returns(uint[]) {
    uint[] memory evens = new uint[](5);
    
    uint counter = 0;
    
    for(uint i = 0; i <= 10; i++ ){
        if(i % 2 == 0){
            evens[counter] = i;
            counter++;
        }
    }
    return evens;    
}


레벨 4

Payable 제어자

payable로 지정된 함수에선 이더리움을 받을 수 잇는 특수 함수이다.

일반적인 API로는 보낼수 없지만 payable로는 보낼수 있다.

이 함수를 실행하기 위해선 컨트렉트에 일정금액을 지불해야 한다.

Contract OnlineStore {
    function buySomething() external payable {
        //함수 실행에 0.001이더가 보내졌는지 체크하는 함수 (> , <로도 이용가능)
        require(msg.value == 0.001 ether);
        transferthing(msg.sender); //이더를 이동..
    }
}

그럼 웹에서는 이 부분 호출하기 위해선

// `OnlineStore`는 자네의 이더리움 상의 컨트랙트를 가리킨다고 가정해야 한다. 
OnlineStore.buySomething({from: web3.eth.defaultAccount, value: web3.utils.toWei(0.001)})

주의 할점은 만약 함수가 payable 제어자가 없는 경우 이더를 보낸다고 하면 함수에서 트랙젝션을 거부를 할것이다.

출금시스템

이더를 보낸 후 출금을 하지 않는 경우 해당 컨트렉트의 이더리움 계좌에 저장되고 갇히게 된다.

그래서 그걸 출금하는 시스템을 같이 개발해야 한다.

contract GetPaid is Ownable {
    //Owner 인지 체크
    function withdraw() external onlyOwner {
        //계좌에 있는 금액을 트렌스퍼한다. 
        owner.transfer(this.balance);
    }
}

그리고 초과 입금시 나머지를 돌려주는 로직을 짤려면 다음과 같다.

uint itemfee = 0.001 ether;
msg.sender.transfer(msg.value - itemFee); //입금하는 사람에게 출금하고 있다. 

만약 판매자가 있고 구매자가 물건을 구입시 이미 판매자가 storage 에 저장되었다는 가정하에

seller.transfer(msg.value) //지정된 판매자에게 금액을 출금하고 있다. 

Keccak256을 통한 난수 생성

솔리디티에서 난수를 만들려면 keccak256해시 함수를 사용하면 된다.

uint randNonce = 0;
uint random = uint(keccak256(now, msg.sender, randNounce)) % 100;
randNonce++;
uint random2 = uint(keccak256(now, msg.sender, randNounce)) % 100;

now => 타임스탬프 값

실행시 0~99까지 난수를 만들 수 있다.

참고사항

이더리움에서는 자네가 컨트랙트의 함수를 실행하면_트랜잭션(transaction)으로서 네트워크의 노드 하나 혹은 여러 노드에 실행을 알리게 되네. 그 후 네트워크의 노드들은 여러 개의 트랜잭션을 모으고, "작업 증명"으로 알려진 계산이 매우 복잡한 수학적 문제를 먼저 풀기 위한 시도를 하게 되네. 그리고서 해당 트랜잭션 그룹을 그들의 작업 증명(PoW)과 함께블록_으로 네트워크에 배포하게 되지.

한 노드가 어떤 PoW를 풀면, 다른 노드들은 그 PoW를 풀려는 시도를 멈추고 해당 노드가 보낸 트랜잭션 목록이 유효한 것인지 검증하네. 유효하다면 해당 블록을 받아들이고 다음 블록을 풀기 시작하지.

이것이 우리의 난수 함수를 취약하게 만드네.

우리가 동전 던지기 컨트랙트를 사용한다고 해보지 - 앞면이 나오면 돈이 두 배가 되고, 뒷면이 나오면 모두 다 잃는 것이네. 앞뒷면을 결정할 때 위에서 본 난수 함수를 사용한다고 가정해보세. (random >= 50은 앞면,random < 50은 뒷면이네).

내가 만약 노드를 실행하고 있다면, 나는오직 나의 노드에만트랜잭션을 알리고 이것을 공유하지 않을 수 있네. 그 후 내가 이기는지 확인하기 위해 동전 던지기 함수를 실행할 수 있지 - 그리고 만약 내가 진다면, 내가 풀고 있는 다음 블록에 해당 트랜잭션을 포함하지 않는 것을 선택하지. 난 이것을 내가 결국 동전 던지기에서 이기고 다음 블록을 풀 때까지 무한대로 반복할 수 있고, 이득을 볼 수 있네.

그럼 이더리움에서는 어떻게 난수를 안전하게 만들어낼 수 있을까? {#-}

블록체인의 전체 내용은 모든 참여자에게 공개되므로, 이건 풀기 어려운 문제이고 그 해답은 이 튜토리얼에를 벗어나네. 해결 방법들에 대해 궁금하다면이 StackOverflow 글을 읽어보게. 하나의 방법은 이더리움 블록체인 외부의 난수 함수에 접근할 수 있도록_오라클_을 사용하는 것이네.

물론, 네트워크 상의 수만 개의 이더리움 노드들이 다음 블록을 풀기 위해 경쟁하고 있으니, 내가 다음 블록을 풀 확률은 매우 낮을 것이네. 위에서 말한 부당한 방법을 쓰는 것은 많은 시간과 연산 자원을 필요로 할 것이야 - 하지만 보상이 충분히 크다면(내가 천억 원을 걸 수 있다든지?), 공격할 만한 가치가 있을 것이네.

그러니 이런 난수 생성은 이더리움 상에서 안전하지는 않지만, 실제로는 난수 함수가 즉시 큰 돈이 되지 않는 한, 자네 게임의 사용자들은 게임을 공격할 만한 충분한 자원을 들이지 않을 것이네.

이 튜토리얼에서는 시연 목적으로 간단한 게임을 만들고 있고 바로 돈이 되는 게 없기 때문에, 우린 구현하기 간단한 난수 생성기를 사용하는 것으로 타협할 것이네. 이게 완전히 안전하지는 않다는 걸 알긴 하지만 말이야.

향후 레슨에서는, 우린_oracle_(이더리움 외부에서 데이터를 받아오는 안전한 방법 중 하나)을 사용해서 블록체인 밖에서 안전한 난수를 만드는 방법을 다룰 수도 있네.

Else 구문

if (zombieCoins[msg.sender] > 100000000) {
    
}else{
    
}


레벨 5

토큰

기본적으로 ERC20 토큰이 있어서 이걸 거래가 가능하다. 이더리움 토큰을 생각해보면 된다.

하지만 게임상에서 게임 수집물에 대한 토큰으로 표기하기에는 한계가 있다..

좀비하나마다 아이디를 부여해야 하기 때문이다.

그래서 ERC721 토큰을 제공한다.

참고 : ERC721 같은 표준을 사용하는 장점은 경매와 플레이어가 좀비를 거래 / 판매하는 방식을 결정하는 에스크로 로직을 계약에서 구현할 필요가 없어진다는 점입니다.

사양을 준수하면 다른 사람이 수집 가능한 ERC721 스크립트 자산의 교환 플랫폼을 만들 수 우리의 ERC721 좀비는 그 플랫폼에서 사용할 수 있습니다.

따라서 자신의 거래 로직을 전개하는 대신 토큰 규격을 사용하는 것은 분명히 장점이 있습니다.

ERC 721 토큰 규격

contract ERC721 {
  event Transfer(address indexed _from, address indexed _to, uint256 _tokenId);
  event Approval(address indexed _owner, address indexed _approved, uint256 _tokenId);

  function balanceOf(address _owner) public view returns (uint256 _balance);
  function ownerOf(uint256 _tokenId) public view returns (address _owner);
  function transfer(address _to, uint256 _tokenId) public;
  function approve(address _to, uint256 _tokenId) public;
  function takeOwnership(uint256 _tokenId) public;
}

토큰 계약 구현시 먼저 할껀 Import 를 해서 가져오는 것이다.

import "./erc721.sol"

//참고로 계약은 다중상속을 허용하고 있다. 
contrat A is B,C {
}
function balanceOf(address _owner) public view returns (uint256 _balance);
//Address 입력시 그 계정에 balance가 얼마나 있는지 조회할 수 있다.
function ownerOf(uint256 _tokenId) public view returns (address _owner);
//해당 토큰 아이디에 address를 리턴해준다.

오버플로우

연산시 만약 uinit8 을 지정한 상태에서 8비트만 가질수 있다. 즉 최대수는 2^8 -1 = 255 이다.

하지만 만약

uint number = 255;
number++; //이 경우 오버플로우 발생함

언더플로우

오버플로우 반대로 음수로 가는 경우 발생될 수 있다.

SafeMath

이를 방지하기 위해 OpenZepplin은 SafeMath라는 라이버러리를 제공한다.

기본적으로 함수는 4개의 기능을 제공한다.

add, sub, mul, div

using SafeMath for uint256; //라이버러리를 사용하겠다고 정의

uint256 a = 5;
uint256 b = a.add(3);
uint256 c = a.mul(2);

내부 소스를 들어다 보면

library SafeMath {

  function mul(uint256 a, uint256 b) internal pure returns (uint256) {
    if (a == 0) {
      return 0;
    }
    uint256 c = a * b;
    assert(c / a == b);
    return c;
  }

  function div(uint256 a, uint256 b) internal pure returns (uint256) {
    // assert(b > 0); // Solidity automatically throws when dividing by 0
    uint256 c = a / b;
    // assert(a == b * c + a % b); // There is no case in which this doesn't hold
    return c;
  }

  function sub(uint256 a, uint256 b) internal pure returns (uint256) {
    assert(b <= a);
    return a - b;
  }

  function add(uint256 a, uint256 b) internal pure returns (uint256) {
    uint256 c = a + b;
    assert(c >= a);
    return c;
  }
}

library 라는 키워드 확인해볼 필요있다.

library 타입으로 지정시 다른 타입을 앞으로 끌어들일 수 있다.

using SafeMath for uint;
uint test = 2;
test = test.mul(3); //인자가 2개가 필요하지만 자동으로 앞 변수(test)를 입력받게 된다.. 

사용법

//num++ 이 방법을 쓰지 말고
num = num.add(1); //이렇게 대체하면 됩니다.

Assert

사용법은 **require **와 비슷하다. 값이 false 일때 반환한다.

그러면 require 와 차이점은 무엇일까.

**require **실패시 가스비가 반환된다. 하지만 **assert **사용시 가스비는 반환되지 않고 false 로 끝난다.

그래서 극단적인 오류의 경우에만 사용해야 한다. 예를 들어 오버플로우나 언더 플로우등..

uint16 , uint32 상의 오버/언더 플로 해결방법

기본적으로 SafeMath 타입은 uint256 이다. 그래서 추가적으로 2개의 라이버러리를 더 가져와야 한다.

SafeMath16, SafeMath32

내부적으로 같지만 타입만 변경되었다.

주석

솔리디티에서 주석은 자바스크립트 과 동일하다.

주석은 **natspec 을 따른다. **

/// @title A contract for basic math operations
/// @author H4XF13LD MORRIS 💯💯😎💯💯
/// @notice For now, this contract just adds a multiply function
contract Math {
  /// @notice Multiplies 2 numbers together
  /// @param x the first uint.
  /// @param y the second uint.
  /// @return z the product of (x * y)
  /// @dev This function does not currently check for overflows
  function multiply(uint x, uint y) returns (uint z) {
    // This is just a normal comment, and won't get picked up by natspec
    z = x * y;
  }
}

@notice 는 이 함수가 하는 일을 설명한다.

@dev 는 추가적으로 개발자에게 할 말을 적는다.


제 6장 웹 연동

Web3.js

이더리움 네트워크내에서 통신을 하기위해선 여러가지 방법이 있지만 주로 JSON-RPC를 통한다. 하지만 형태는 직접 보기가 불편한 점이 있다. 그래서 그걸 인터페이스로 쉽게 구현하는 라이버러리가 있다. Web3.js 를 뜻한다.

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 에서 계약 내 함수 호출시 다음과 같은 형태로 가능하다.

  1. 좀비를 만들고
  2. send라는 트랙잭션이 발생된다.
CryptoZombies.methods.createRandomZombie("Vitalik Nakamoto 🤔")
  .send({ from: "0xb60e8dd61c5d32be8058bb8eb970870f07233155", gas: "3000000" })

Web3 라이버러리 주입

web3 설치시 여러가지 방법이 있을 수 있다.

// Using NPM
npm install web3

// Using Yarn
yarn add web3

// Using Bower
bower install web3

추가적으로 스크립트로 가져오는 방법인

<script language="javascript" type="text/javascript" src="web3.min.js"></script>

Web3 Provider

Web3 Provider 은 이더리움 서버를 어떤 걸로 할지 설정해준다. 자신의 네트워크로 지정해도 상관은 없다. 그리고 메인이나 테스트넷등에 접속시 쉽게 하기 위해서 Infura 라는 서비스도 제공되어 진다.

Infura 를 공급자(Provider)로 사용하는 경우 자신의 노드를 설정하고 유지할 필요없이 Ethereum 블록 체인으로 메세지를 보내고 받을 수 있다.

var web3 = new Web3(new Web3.providers.WebsocketProvider("wss://mainnet.infura.io/ws"));

DAPP의 경우 읽는 것 뿐아니라 블록체인에 Write 할 것이다. 그럼 이 사용자들이 개인 키로 트랙잭션에 서명 할 수 있는 방법이 필요하다.

그렬경우 대표적으로 인기가 많은 것은 MetaMask이다.

하지만 만약 Metamask 를 설치를 하지 않을 경우에도 오류 메세지를 보여주는 편이 좋다.

window.addEventListener('load', function() {

  // Checking if Web3 has been injected by the browser (Mist/MetaMask)
  if (typeof web3 !== 'undefined') {
    // Use Mist/MetaMask's provider
    web3js = new Web3(web3.currentProvider);
  } else {
    // Handle the case where the user doesn't have web3. Probably 
    // show them a message telling them to install Metamask in 
    // order to use our app.
  }

  // Now you can start your app & access web3js freely:
  startApp()

})

Web3.js 은 두가지가 필수로 주어져야 한다.

  • address : 추후 스마트 계약을 배포 한 후에 주어지는 주소를 뜻한다
  • ABI : Application Binary Interface 를 줄인 말이며 함수들과 기타 정보들을 JSON으로 모아놓은 형태를 말한다. 배포시 얻을 수 있다.
// Instantiate myContract
var myContract = new web3js.eth.Contract(myABI, myContractAddress);

스마트 계약 연동

**Web3.js 는 작성한 스마트 계약과의 연동시 **주소와 ABI 가 필요합니다.

Address

스마트 계약 작성후 그 계약을 컴파일 한 후 이더리움에 배포를 하는 과정을 거칩니다. 배포를 하게 되면 이더리움 해당 되는 넷에 그 계약이 머물고 있는 주소를 얻을수 있다.

ABI (Application Binary Interface 응용 프로그램 바이너리 인터페이스)

배포 전 컴파일하게 되면 web3.js 가 이해할 수 있는 JSON 향태의 파일을 제공해준다. 그럼 이 걸 주소와 같이 저장해서 사용을 하면 된다.

이 2개가 준비되었다고 가정하면 Web3 에서는 인스턴스화 할 수 있다.

var myContract = new Web3js.eth.Contract(myABI, myContractAddress);

Call & Send

자 이제 모든 준비는 다 된 상태이다. 그럼 함수들을 호출을 해보자. 함수 호출시 call과 send를 사용한다.

Call: View, Pure 함수로 주로 사용된다. 이건 로컬 노드에서 실행되기 때문에 블록체인에서 transaction 을 실행하지 않는다.

View,Pure 함수는 가스소모가 없다.

myContract.methods.myMethod(123).call()
  • send: 트랙잭션이 발생되며 블록체인내에서 변화가 이루어진다. View,Pure 함수를 제외하고 함수들이 send로 불리어진다.

send함수를 실행시 가스가 소모되며 만약 메타메스크를 실행한 상태라면 그 메타메스크가 열리면서 함수를 실행을 할 수 있게 된다. transaction에는 submit 도 가능하다.

myContract.methods.myMethod(123).send()

유저 계정 정보를 획득하기

MetaMask 를 통해 Web3 은 유저정보를 실시간으로 받아올 수 있다.

var userAccount = web3.eth.accounts[0]

만약 유저 계정을 바뀌는 경우라면 어떻게 되는 것일까.. 크롬에서 metamask를 켜고 유저정보를 실시간으로 변경시 그 정보를 업데이트를 해줘야 한다.

그걸 하기 위해선 매초마다 체크를 해서 업데이트를 해주면 된다.

var accountInterval = setInterval(function() {
    //변경이 된 경우라면
    if(web3.eth.accounts[0] !== userAccount) {
        userAccount = web3.eth.accounts[0];
        updateInterface();
    }
}, 100);
getZombieDetails(id)
.then(function(zombie) {
  // Using ES6's "template literals" to inject variables into the HTML.
  // Append each one to our #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>`);
});

Send 함수

  • send트랙잭션을 수행하면 from해당되는 기능을 호출하는 사람의 주소가 필요하다. (msg.sender) . 그 후에 MetaMask 가 팝업으로 나와서 submit(전송확인) 할 것인지 물어본다.
  • send시 거래 비용으로 가스가 든다.
  • send를 하는 순간 블록에 노드를 업데이트 하므로 최소 15이상의 시간이 걸릴 수 있으므로 그에 대한 UI 를 고려 해야 할 것이다. 즉 비동기 함수가 필요할 것이다.
... Solidity 코드
function createRandomZombie(string _name) public {
  require(ownerZombieCount[msg.sender] == 0);
  uint randDna = _generateRandomDna(_name);
  randDna = randDna - randDna % 100;
  _createZombie(_name, randDna);
}

.... 비동기로 처리 되는 부분을 눈여겨 봐야 한다.
.... Web3.js 코드
function createRandomZombie(name) {
  // This is going to take a while, so update the UI to let the user know
  // the transaction has been sent
  $("#txStatus").text("Creating new zombie on the blockchain. This may take a while...");
  // Send the tx to our contract:
  return CryptoZombies.methods.createRandomZombie(name)
  .send({ from: userAccount })
  .on("receipt", function(receipt) {
    $("#txStatus").text("Successfully created " + name + "!");
    // Transaction was accepted into the blockchain, let's redraw the UI
    getZombiesByOwner(userAccount).then(displayZombies);
  })
  .on("error", function(error) {
    // Do something to alert the user their transaction has failed
    $("#txStatus").text(error);
  });
}

여기에서 이벤트 리스너인 receipt 와 error 부분이 나온다.

  • receipt : 트랙잭션이 이더리움의 블록에 포함되면 트리거된다. 좀비가 계약서에 저장되었음을 의미한다.
  • error : 가스가 불충분하거나 거래가 블록에 업데이트가 되지 않을 경우 발동된다. 그럼 여기에선 오류 발생됨을 알려야 할것이다.

그리고 전송시 가스를 코드에서 지정하거나 MetaMask .에서 지정하는 방법이 있을 수 있다.

.send({ from: userAccount, gas: 3000000 }) // 이렇게 코드에서 지정하거나 MetaMask 로 사용자가 가스비를 설정하게끔 할수 있다.

Payable 함수

// zombieHelper 에 함수를 호출할 수 있다. 
function levelUp(uint _zombieId) external payable {
  require(msg.value == levelUpFee);
  zombies[_zombieId].level++;
}

Wei란?

1 ether = 10^18 wei 로 계산된다.

그래서 web3에선 편하게 1이더를 wei 로 쉽게 변환해준다.

web3js.utils.toWei("1", "ether");

levelUpFee 가 0.001 이더로 설정되어있기 때문에 우리는 0.001 이더를 보낼 수 있다.

CryptoZombies.methods.levelUp(zombieId)
.send({ from: userAccount, value: web3js.utils.toWei("0.001", "ether") })

이벤트 구독

zombieFactory.sol 에서 새로운 좀비 호출시 NewZombie라는 이벤트를 트리거할 수 있다. web3js 에서도 이벤트 발생이 가능하다.

cryptoZombies.events.NewZombie()
.on("data", function(event) {
  let zombie = event.returnValues; //이벤트 결과값을 여기로 리턴받음
  console.log("A new zombie was born!", zombie.zombieId, zombie.name, zombie.dna);
}).on("error", console.error);

어떤 좀비라도 DAPP 에서 생성이 되면 로그를 보여주게 된다. 현재 유저뿐만 아니라 모든 좀비에 대해서 이벤트가 발생을 하게 된다. 하지만 만약 현재 유저에만 보여주고 싶다면?

indexed 키워드 사용

이벤트에서 현재 유저에만 이벤트가 가도록 하고 싶다면 indexed 키워드를 지정하면 된다.

ERC721상의 이벤트 중 하나인 전송 부분에서 살펴볼 수 있다.

event Transfer(address indexed _from, address indexed _to, uint256 _tokenId);

_from,_to 가 indexed 키워드가 붙여져 있다.

// Use `filter` to only fire this code when `_to` equals `userAccount`
cryptoZombies.events.Transfer({ filter: { _to: userAccount } })
.on("data", function(event) {
  let data = event.returnValues;
  // The current user just received a zombie!
  // Do something here to update the UI to show it
}).on("error", console.error);

지난 이벤트 조회

getPastEvents 를 사용하여 지난 이벤트를 조회도 가능하다. fromBlock, toBlock 로 이벤트 로그들을 살펴볼수 있다.

cryptoZombies.getPastEvents("NewZombie", { fromBlock: 0, toBlock: "latest" })
.then(function(events) {
  // `events` is an array of `event` objects that we can iterate, like we did above
  // This code will get us a list of every zombie that was ever created
});

Web3.js 이벤트 및 메타 마스크

현재로서는 웹소켓을 이용하여 이벤트를 구독할 수 있다. 최신 1.0 web3.js 내용이다.

아직 메타메스크는 Provider 로써 제공을 못 해주고 있다. 그래서 infura 를 사용하여 만들수 있다.

var web3Infura = new Web3(new Web3.providers.WebsocketProvider("wss://mainnet.infura.io/ws"));
var czEvents = new web3Infura.eth.Contract(cryptoZombiesABI, cryptoZombiesAddress);


광고한번 눌러주시면 크게 힘이 됩니다. 


이번 포스팅은 제 딸들 코인인 수지토큰 을 발행해보겠습니다.

우선 토큰과 코인은 차이가 있습니다. 






그럼 시작해보겠습니다. 


우선 결과물은 다음과 같습니다.

이더리움에 올릴수 있는 토큰은 ERC20 규격으로 만들수 있다. 이 토큰을 가지고 ICO에 올려서 서로간의 거래를 할 수 있습니다. 

그럼 시작해보자. 

ERC20 위키바로가기

규격 인터페이스 형태는 다음과 같다. 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
contract ERC20Interface {
    //총 갯수
    function totalSupply() public constant returns (uint);
    //주어진 토큰오너에 대한 잔액조회
    function balanceOf(address tokenOwner) public constant returns (uint balance);
    //소유자가 승인 할 수 는 토큰 양을 반환합니다. 
    function allowance(address tokenOwner, address spender) public constant returns (uint remaining);
    //to 에게 토큰을 전송한다. 
    function transfer(address to, uint tokens) public returns (bool success);
    //트큰을 원래주인으로부터 다른사람에게 전송함을 승인한다. 
    function approve(address spender, uint tokens) public returns (bool success);
    //from -> to 을 토큰을 전송 : 성공/실패
    function transferFrom(address from, address to, uint tokens) public returns (bool success);
 
    //전송 이벤트
    event Transfer(address indexed from, address indexed to, uint tokens);
    //승인 이벤트
    event Approval(address indexed tokenOwner, address indexed spender, uint tokens);
}
cs

필요한 지식은 다음과 같다.. 

  • DAPP
  • Truffle
  • Node
  • MetaMask
  • MyEtherWallet
  • Javascript

여기에선 Truffle 이더리움 프레임워크를 이용해서 쉽게 만들어볼것이다. 

우선 설치를 해보자. 

npm install -g truffle

그리고 원하는 위치에 폴더를 만들고 초기화를 진행한다. 

1
2
3
mkdir sugi-coin
cd sugi-coin
truffle init
cs



처음 트리 구조는 위와 같다.

그리고 오픈재플린( OpenZepplin ) 을 라이버러리를 설치를 해보자. 

유용한 계약들을 손쉽게 쓸수 있도록 제공해주는 DAPP 개발시 필수 라이버러리이다. 

토큰 규약 말고도 여러가지가 미리 포함되어 있다. 

1
npm install zeppelin-solidity -save
cs

그럼 contract 를 작성해보자.

contract 폴더내에 sujicoin.sol 을 작성한다. StandardToken 을 상속받는다. 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
pragma solidity ^0.4.18;
import "zeppelin-solidity/contracts/token/ERC20/StandardToken.sol";
 
contract Sujicoin is StandardToken {
    string public name = "Sujicoin"
    string public symbol = "SUJI"//통화단위
    uint public decimals = 2//자리수
    uint public INITIAL_SUPPLY = 10000 * (10 ** decimals); //초기 공급량
 
    //생성자
    function Sujicoin() public {
        balances[msg.sender] = INITIAL_SUPPLY;
    }
}
cs

그리고 배포를 위해 migrations 폴더에 파일을 새로 만든다. 

1
2_deploy_sujicoin.js
cs

소스는 아래와 같이 작성한다.

1
2
3
4
5
var SujiCoin = artifacts.require("./Sujicoin.sol");
 
module.exports = function(deployer) {
    deployer.deploy(SujiCoin);
};
cs

그리고 루트 폴더에 보면 truffle.js 파일이 있을 것이다. 아래와 같은 소스를 넣어준다. 

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
require('dotenv').config();
const Web3 = require("web3");
const web3 = new Web3();
const WalletProvider = require("truffle-wallet-provider");
const Wallet = require('ethereumjs-wallet');

var rinkebyPrivateKey = new Buffer("private key", "hex")
var rinkebyWallet = Wallet.fromPrivateKey(rinkebyPrivateKey);
var rinkebyProvider = new WalletProvider(rinkebyWallet, "https://rinkeby.infura.io/");


module.exports = {
networks: {
development: {
host: "localhost",
port: 8545,
network_id: "*" // Match any network id
},
rinkeby: {
provider: rinkebyProvider,
gas: 4600000,
gasPrice: web3.toWei("20", "gwei"),
network_id: "3",
}
}
};
cs


[ RINKEBY_PRIVATE_KEY ] 에 지갑에서 private key를 넣어주자. 

여기에서는 Metamask 를 이용한다.

Private key 추출은 Metamask 를 통해서 할수 있다. 



서버는 Rinkeby 테스트 서버를 이용한다. 

그리고 배포를 진행해본다. 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
>> truffle deploy --network rinkeby
Compiling .\contracts\SujiCoin.sol...
Compiling zeppelin-solidity/contracts/token/ERC20/StandardToken.sol...
Compiling zeppelin-solidity\contracts\math\SafeMath.sol...
Compiling zeppelin-solidity\contracts\token\ERC20\BasicToken.sol...
Compiling zeppelin-solidity\contracts\token\ERC20\ERC20.sol...
Compiling zeppelin-solidity\contracts\token\ERC20\ERC20Basic.sol...
Writing artifacts to .\build\contracts
 
Using network 'rinkeby'.
 
Running migration: 1_initial_migration.js
  Replacing Migrations...
  ... 0x2ce47755f6112ab384a14910656eddb18fa94061cc1dc0349f7b899fb3ba9f84
  Migrations: 0x5903b751a054c281f12a354a8d9bb2304a584447
Saving successful migration to network...
  ... 0x6a7b74a8d897593042da8b7f349a39e5238f5ed2dce302965ef50ccc58b0260c
Saving artifacts...
Running migration: 2_deploy_hamburgercoin.js
  Deploying SujiCoin...
  ... 0x1d52f1e2805d04640d70a8f109f07292bdde2469dd377ac3315964956563ef53
  SujiCoin: 0x2e7b9c9c7c74bac74d069e57128dc66898ff41a9
Saving artifacts...
cs

최종적으로 나온 SujiCoin: 0x2e7b9c9c7c74bac74d069e57128dc66898ff41a9

0x.... 이 부분이 현재 올라간 계약 부분이다. 그럼 잘 올라갔는지 확인해보자. 

여기에서는 Rinkeby test net을 기본으로 한다. 

https://rinkeby.etherscan.io/address/0x2e7b9c9c7c74bac74d069e57128dc66898ff41a9

이 주소로 가서 테스트 서버에 정상적으로 올라갔는지 확인 해본다. 

성공적으로 올라간 것을 볼 수 있다. 그러면 이걸 다시 제대로 등록 되는 지 확인해보자. 

Address( 0x2e7b9c9c7c74bac74d069e57128dc66898ff41a9 ) 을 복사를 해서 Metamask 토큰에 넣어보자.


토큰 등록하기

주소 입력하기

위와 같이 SUJI 토큰이 등록된 걸 볼 수 있다.

그럼 전송을 서로간에 해보자.

우선 https://www.myetherwallet.com/ 으로 가보자.

오른쪽에 보면 Metamask 연동하는 부분이 보인다. 클릭하자.



계정을 바꾸는 방법은 오른쪽 상단 클릭시 변경가능하다. 

이렇게 해서 수지토큰 이 탄생되었다. 



예제로 배우는 DAPP 개발해보기 - 복권편

이더리움 블록체인 위에 어플리케이션 형태로 올려서 개발할 수 있는 DAPP을 예제로 배워보겠다. 
처음 예제는 복권(Lottery) 이다. 

내용은 최근 세미나 한 자료를 기준으로 진행한다. 


예제 자료는 다음의 위치에서 볼 수 있다. 

https://github.com/bear2u/lottery_exam1


결과물은 오른쪽과 같다. 

지갑을 우선 설치를 해보자. 

https://metamask.io/ 에서 크롬 확장 프로그램인 설치를 하자. 

로그인을 비밀번호를 입력 후 하면 되는데 문자열이 모니크 키값으로 주어지는데 꼭 다른 곳에 저장을 해놓자. 

  

로그인시 보유한 이더와 서버주소를 확인 할 수 있다. 테스트 서버를 우선 설정해보자. 


왼쪽 하단 서버 선택을 Rinkeby Test Net 으로 지정하자. 

개발을 해서 테스트를 할려면 이더(코인)가 필요하다. 

무료 이더를 받을 수 있는 방법은 2가지가 있다. 

  • http://rinkeby-faucet.com : 지갑 계정을 복사를 해서 해당 사이트에 넣으면 0.001 이더를 보내준다. (테스트 서버에 한해)

  • https://faucet.rinkeby.io/ : 소셜을 통해서 약 18이더정도를 받을 수 있다. 사용 방법은 피드에 내 지갑 주소를 올려서 그 해당 링크를 넣으면 된다. 



일반적인 아키텍처와 비교해서 차이점은 이더리움 블록체인 서버에 연결하기 위해 web3 라이버러리를 이용할 수 있다.

현재 예제에서는 1.0.0-beta.26 을 이용한다. 이후 버전 이용시 오류가 다소 나오는 것 같다. '

대소문자를 조심해야 한다. 


오타 : Proof of Work (POW)




http://remix.ethereum.org/ 에서 온라인 컴파일 및 테스팅을 진행 할 수 있다. 


테스팅 환경을 선택을 가능하다. 

우선 로컬로 지정해서 시작한다. 

이 후엔 서버를 선택해서 진행가능하다. 



  • 실행환경 지정

  • 생성 함수를 어떤 식으로 지정 할 수 있는지 지정

  • 각각의 함수를 테스팅 할 수 있다. 





베팅하는 걸 알아보자. 


Account를 기본이나 다른 걸로 설정한다. JVM 설정시 기본 5개는 주어진다. 

1이더 이상 값을 설정한다.

그리고 하단에 Enter를 통해서 베팅한다.




















Account를 다른 걸로 선택해서 Enter를 해서 여러명을 입장한다. 


여러명을 베팅을 시켜서 돈이 다 나갔는지 확인해본다. 

현재 4명이 들어간 상태이다. 



유저가 제대로 들어왔는지 확인해본다. 총 4명인걸 확인한다.



그리고 매니저를(처음 만든 계정) 통해서 pickWinner 클릭한다. 

그럼 랜덤으로 이긴 사람에게 돈이 들어간걸 볼 수 있다. 102.xxxxx 이더로 들어갔다. 

콘솔창으로 오류 및 진행사항들을 확인 할 수 있다.

그리고 이 작성된 솔리디티 스크립트를 함수가 제대로 실행되는지 테스팅 해보자. mocha 로 진행한다.

 

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
const assert = require('assert');
const ganache = require('ganache-cli');
const Web3 = require('web3');
const provider = ganache.provider();
const web3 = new Web3(provider);
 
const { interface, bytecode } = require('../compile');
 
let lottery;
let accounts;
 
beforeEach(async () => {
  accounts = await web3.eth.getAccounts();
 
  lottery = await new web3.eth.Contract(JSON.parse(interface))
      .deploy({ data: bytecode })
      .send({ from: accounts[0], gas: '1000000'});
 
  lottery.setProvider(provider);
});
 
describe('Lottery Contract', () => {
  it('deploys a contract', () => {
    assert.ok(lottery.options.address);
  });
 
  it('allows one account to enter', async () => {
    await lottery.methods.enter().send({
      from: accounts[0],
      value: web3.utils.toWei('0.02','ether'//'2000000000000000'
    });
 
    const players = await lottery.methods.getPlayers().call({
      from: accounts[0]
    });
 
    //저장된 사람이 계정에 있는지 체크
    assert.equal(accounts[0], players[0]);
    //계정 사이즈 체크
    assert.equal(1, players.length);
  });
 
  it('allows multiple accounts to enter', async () => {
    await lottery.methods.enter().send({
      from: accounts[0],
      value: web3.utils.toWei('0.02','ether'//'2000000000000000'
    });
 
    await lottery.methods.enter().send({
      from: accounts[1],
      value: web3.utils.toWei('0.02','ether'//'2000000000000000'
    });
 
    await lottery.methods.enter().send({
      from: accounts[2],
      value: web3.utils.toWei('0.02','ether'//'2000000000000000'
    });
 
    const players = await lottery.methods.getPlayers().call({
      from: accounts[0]
    });
 
    //저장된 사람이 계정에 있는지 체크
    assert.equal(accounts[0], players[0]);
    assert.equal(accounts[1], players[1]);
    assert.equal(accounts[2], players[2]);
    //계정 사이즈 체크
    assert.equal(3, players.length);
  });
 
  it('requires a minimum amount of ether to enter', async () => {
    try{
      await lottery.methods.enter().send({
        from: accounts[0],
        value: 0
      });
      assert(false);
    } catch (err) {
      assert.ok(err);
    }
  });
 
  it('only manager can call pickWinner' , async () => {
    try{
      await lottery.methods.pickWinner().send({
        from: accounts[1]
      });
      assert(false);
    }catch(err) {
      assert(err);
    }
  });
 
  if('sends money to the winnder and resets the players array', async () => {
    await lottery.methods.enter().send({
      from: accounts[0],
      value: web3.utils.toWei('2''ether')
    });
 
    const initialBalance = await Web3.eth.getBalance(accounts[0]);
 
    await lottery.methods.pickWinner().send({ from: accounts[0] });
 
    const finalBalance = await web3.eth.getBalance(accounts[0]);
 
    const difference = finalBalance - initialBalance;
    console.log(finalBalance - initialBalance);
    assert(difference > web3.utils.toWei('1.8''ether'));
 
  });
});
 
cs


이제 컴파일 해보자. 

1
2
3
4
5
6
7
8
9
const path = require('path');
const fs = require('fs');
const solc = require('solc');
 
const lotteryPath = path.resolve(__dirname, 'contracts''Lottery.sol');
const source = fs.readFileSync(lotteryPath, 'utf8');
 
module.exports = solc.compile(source, 1).contracts[':Lottery'];
 
cs


그리고 배포를 해보자. 배포를 하면 시간이 다소 걸린다. 실제 서버로 올리기 때문이다. 

ABI 내용도 나오는 걸 볼 수 있다. 

1
2
3
4
5
6
7
8
node compile
node deploy
 
//결과
Attempting to deploy from account 0xBC9aAd0B1598a9388f3535272376006eeaF1eea2 // contract address
[{"constant":true,"inputs":[],"name":"manager","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"pickWinner","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"getPlayers","outputs":[{"name":"","type":"address[]"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"enter","outputs":[],"payable":true,"stateMutability":"payable","type":"function"},{"constant":true,"inputs":[{"name":"","type":"uint256"}],"name":"players","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"inputs":[],"payable":false,"stateMutability":"nonpayable","type":"constructor"}]
Contract deployed to 0x43eD67e834c9588bd8552BbC54Bc1d565C6C8Cf7
 
cs



그리고 잘 배포가 되었는지 확인해보자. 

https://rinkeby.etherscan.io/

에서 계약 주소를 넣어서 확인 할 수 있다.  물론 현재 서버는 RINKEBY 테스트 서버이다.  

이제 웹 클라이언트 화면을 만들 차례이다. 

현재 지갑을 가져온다. 

1
2
3
4
5
6
7
import Web3 from 'web3';
 
const web3 = new Web3(window.web3.currentProvider);
 
export default web3;
 
 
cs


여기에서 지갑 가져오는 위치를 다른 곳으로 바꿀 수도 있다. 

1
2
3
4
5
6
7
8
if (typeof web3 !== 'undefined') {
  web3 = new Web3(web3.currentProvider);
else {
  // set the provider you want from Web3.providers
  web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:8545"));
}
 
 
cs


그리고 lottery.js 을 통해 현재 솔리디티로 작성된 내용을 가져온다. 계약 주소와 abi 내용을 입력하면 된다.

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
import web3 from './web3';
 
const address = '0x6D02E4aD1E1870E96E41E5735A2ddb02165C213b';
 
const abi = [
  {
    "constant"true,
    "inputs": [
      
    ],
    "name""manager",
    "outputs": [
      {
        "name""",
        "type""address"
      }
    ],
    "payable"false,
    "stateMutability""view",
    "type""function"
  },
  {
    "constant"false,
    "inputs": [
      
    ],
    "name""pickWinner",
    "outputs": [
      
    ],
    "payable"false,
    "stateMutability""nonpayable",
    "type""function"
  },
  {
    "constant"true,
    "inputs": [
      
    ],
    "name""getPlayers",
    "outputs": [
      {
        "name""",
        "type""address[]"
      }
    ],
    "payable"false,
    "stateMutability""view",
    "type""function"
  },
  {
    "constant"false,
    "inputs": [
      
    ],
    "name""enter",
    "outputs": [
      
    ],
    "payable"true,
    "stateMutability""payable",
    "type""function"
  },
  {
    "constant"true,
    "inputs": [
      {
        "name""",
        "type""uint256"
      }
    ],
    "name""players",
    "outputs": [
      {
        "name""",
        "type""address"
      }
    ],
    "payable"false,
    "stateMutability""view",
    "type""function"
  },
  {
    "inputs": [
      
    ],
    "payable"false,
    "stateMutability""nonpayable",
    "type""constructor"
  }
];
 
 
 
export default new web3.eth.Contract(abi, address);
 
 
cs








+ Recent posts