본문 바로가기
경일/Block-chain

[Block-chain / 블록체인] Truffle(트러플) 활용하여 contract(컨트랙트) 배포

by dev_kong 2022. 7. 12.
728x90
728x90

0. 목차

1. 개요

2. truffle 없이 contract 배포

3. truffle 활용하여 contract 배포

1. 개요

저번 포스팅에서는 geth console에서 컨트랙트를 배포해 보았다.

오늘은 javascript 코드 작성을 통해 컨트랙트를 배포 해보며 전체적인 흐름을 이해하고,
truffle 을 활요한 컨트랙트 배포를 해보려 한다.

2. truffle 없이 contract 배포

2-1. UTC 파일 복호화 하여 개인키 가져오기

이전의 포스팅에서 javascript에서 web3 라이브러리를 통해 트랜잭션을 구현한 적이 있는데,

그때 ehtereumjs-tx 라이브러리를 이용해서 서명을 만들었었는데

위 라이브러리를 이용해서 서명을 만들기 위해서는 개인키가 필요하다.


사실 오늘 포스팅에서 사용은 하지 않지만, 다음 포스팅에 사용할 예정임

geth네트워크를 구현할 때 만들었던 node 디렉토리 안에 keystrore라는 디렉토리가 있고,

그 디렉토리 안에는 계정정보가 담겨 있는 UTC 파일이 들어있다.

 

UTC파일은 privateKey를 양방향 암호화가 되어있는 파일이다.

즉, 해당 파일을 복호화를 하면 개인키를 구할 수 있다는 뜻이다.

 

양방향 암호화지만 salt가 존재하는데 그게 처음 계정을 생성할 때 지정했던 비밀번호이다.

 

UTC 파일을 복호화 하는 라이브러리는 많은 방법이 있겠지만

난 keythereum을 통해 복호화를 진행하려고 한다.

 

npm init -y
npm i keythereum web3

 

설치를하고

 

const keythereum = require('keythereum');
const path = require('path');
const address = 'eb657c069c9b51cac4d587e7dc4cc26713912c5a'; //UTC 파일에 적혀있는 address
const dir = path.join(__dirname);

const keyObject = keythereum.importFromFile(address, dir);
const privateKey = keythereum.recover('1234', keyObject).toString('hex'); //recover first parameter -> salt

console.log(privateKey); //private Key 출력

 

테스트를 돌려보니 프라이빗 키가 잘나온다.
..다음에 쓸거니까 잘 기억해두자...^^

 

2-2. contract 작성

이번에는 contract파일을 새로 만들어서 solidity 코드를 작성해보자.

 

pragma solidity ^0.8.15;

contract HelloWorld{
  string public value;

  constructor(){
    value = "Hello World";
  }

  function getValue()public view returns(string memory){
    return value;
  }

  function setValue(string memory _v)public{
    value = _v
  }
}

 

저번포스팅이랑 똑같은 내용의 contract이다.

solidity에도 js의 getter, setter 함수와 똑같은 내용이 존재한다.

 

현재 getValue가 getter, setValue가 setter의 역할을 하는데,

solidity에서는 프로퍼틔의 접근제한자를 public으로 설정하면, getter함수는 자동으로 생성된다.

 

즉,

pragma solidity ^0.8.15;

contract HelloWorld{
  string public value;

  constructor(){
    value = "Hello World";
  }

  function setValue(string memory _v)public{
    value = _v
  }
}

 

getValue는 없어도 된다.

이제 작성한 contract를 compile을 진행해야한다.

 

저번포스팅에서는 solc 라이브러리를 cli로 실행시켜서 컴파일된 파일을 만들었는데,

이번엔 cli가 아닌 js 코드를 이용해 컴파일 해서 파일을 만들어 보려한다.

 

 

const solc = require('solc');
const fs = require('fs');
const path = require('path');

class Contract {
  static compile(_fileName) {
    const contractPath = path.join(__dirname, '../contracts/', _fileName);
    const content = fs.readFileSync(contractPath, 'utf-8');

    const data = JSON.stringify({
      language: 'Solidity',
      sources: {
        [_fileName]: {
          content,
        },
      },
      settings: {
        outputSelection: {
          '*': {
            '*': ['*'],
          },
        },
      },
    });

    const compiled = JSON.parse(solc.compile(data));
    console.log(compiled)
  }
}

Contract.compile('helloWorld.sol'); // 객체 출력됨

 

이렇게 작성해주면 된다.

위코드를 실행했을때 에러없이 잘 돌아간다면 성공이다.

 

data 객체의 스펠링에 주의해야함....

저번 포스팅에서 컴파일을 하면 abi파일과, bin 파일이 아웃풋 되는 걸 확인 할 수 있었는데,

 

이 둘을 확인하는 메서드를 추가로 작성해보자.

 

const solc = require('solc');
const fs = require('fs');
const path = require('path');

class Contract {
  static compile(_fileName) {
    const contractPath = path.join(__dirname, '../contracts/', _fileName);
    const content = fs.readFileSync(contractPath, 'utf-8');

    const data = JSON.stringify({
      language: 'Solidity',
      sources: {
        [_fileName]: {
          content,
        },
      },
      settings: {
        outputSelection: {
          '*': {
            '*': ['*'],
          },
        },
      },
    });

    const compiled = JSON.parse(solc.compile(data));
    this.writeOutput(compiled);
  }

  static writeOutput(_compiled) {
    // console.log(_compiled.contracts['helloWorld.sol'].HelloWorld.abi); abi 확인용
    for (const contractFileName in _compiled.contracts) {
      const [contractName] = contractFileName.split('.');
      console.log(_compiled.contracts[contractFileName][contractName].abi);
      console.log(
        _compiled.contracts[contractFileName][contractName].evm.bytecode.object
      );
    }
  }
}

Contract.compile('HelloWorld.sol');

 

출력하려는 값이 depth가 깊어서 조금 난잡하긴하지만 이렇게 하면 잘 찍힌다.

주의 할점은 solidity 코드가 작성된 파일의 첫글자는 무조건 대문자로 작성해야 위의 코드가 적용된다!!

 

음 콘솔로그가 잘 찍힌다.

 

그럼 이제 이 내용들을 JSON 형식으로 만들어서 파일로 생성을 해주자

루트디렉토리에 build 디렉토리를 하나 생성해주고,

코드를 수정해주자.

 

const solc = require('solc');
const fs = require('fs-extra');
const path = require('path');

class Contract {
  static compile(_fileName) {
    const contractPath = path.join(__dirname, '../contracts/', _fileName);
    const content = fs.readFileSync(contractPath, 'utf-8');

    const data = JSON.stringify({
      language: 'Solidity',
      sources: {
        [_fileName]: {
          content,
        },
      },
      settings: {
        outputSelection: {
          '*': {
            '*': ['*'],
          },
        },
      },
    });

    const compiled = JSON.parse(solc.compile(data));
    return this.writeOutput(compiled); // 수정함.
  }

  static writeOutput(_compiled) {
    // console.log(_compiled.contracts['helloWorld.sol'].HelloWorld.abi); abi 확인용
    for (const contractFileName in _compiled.contracts) {
      const [contractName] = contractFileName.split('.');

      const contract = _compiled.contracts[contractFileName][contractName];

      const { abi } = contract;
      const {
        evm: {
          bytecode: { object: bytecode },
        },
      } = contract;

      const obj = {
        abi,
        bytecode,
      };

      const buildPath = path.join(
        __dirname,
        '../build',
        `${contractName}.json`
      );

      fs.outputJSONSync(buildPath, obj);
      return [abi, bytecode];
    }
  }
}

module.exports = { Contract }

 

 

2-3. web3 연결 (싱글톤)

 

이번에는 싱글톤 class를 작성해서 web3를 연결할 수 있는 코드를 작성해보려고 한다.

 

const Web3 = require('web3');

let instance;
class Client {
  constructor(_url) {
    if (instance) return instance;
    this.web3 = new Web3(
      new Web3.providers.WebsocketProvider('ws://127.0.0.1:9005')
    );
  }
}

module.exports = { Client };

 

위와 같이 작성하면 여러개의 인스턴스를 생성해도 같은 메모리주소를 참조하게끔 할 수 있다.

준비가 드디어 다 됐다.

 

이제 컨트랙트를 배포 해보자..

 

2-4. contract 배포

루트에 index.js 하나 만들어서 작성해보자.

 

const { Contract } = require('./compile/compile.js');
const { Client } = require('./compile/client.js');

const [abi, bytecode] = Contract.compile('HelloWorld.sol');
const client = new Client('ws://127.0.0.1:9005');

 

우선 모듈 가져오고 실행시켜주자.

 

const { Contract } = require('./compile/compile.js');
const { Client } = require('./compile/client.js');

const [abi, bytecode] = Contract.compile('HelloWorld.sol');
const client = new Client('ws://127.0.0.1:9005');

const txObject = {
  data: bytecode,
};

const contract = new client.web3.eth.Contract(abi);

contract
  .deploy(txObject)
  .send({ from: '0xeb657c069c9b51cac4d587e7dc4cc26713912c5a', gas: 3000000 })
  .then((instance) => {
    console.log(instance.options.address); // CA
  });

 

then / catch 구문 개극혐 이니까.. async / await 으로 변경해주자.

 

const { Contract } = require('./compile/compile.js');
const { Client } = require('./compile/client.js');

const [abi, bytecode] = Contract.compile('HelloWorld.sol');
const client = new Client('ws://127.0.0.1:9005');

const txObject = {
  data: bytecode,
};

const contract = new client.web3.eth.Contract(abi);

async function init() {
  const instance = await contract
    .deploy(txObject)
    .send({ from: '0xeb657c069c9b51cac4d587e7dc4cc26713912c5a', gas: 3000000 });

  console.log(instance.options.address);
}

init();

 

요렇게 변경해주면 된다.

 

해당 코드를 실행시키면 트랜잭션이 발생하고, 발생된 트랜잭션은 txpool 에 담긴다.

그리고 바로 await이 실행 되는 것이 아니라,

현재 txpool에 있는 transaction의 내용이 블록에 담겼을때(-> 블록 마이닝 됐을때) 실행된다.

 

블록이 마이닝되고 트랜잭션이 블록에 잘 담기면,

console.log가 동작하는데 출력되는 내용은 배포된 컨트랙트의 CA이다.

 

해당 CA를 이용해서 자동 생성 되는 getter 함수를 실행해보자.

 

async function init() {
  const instance = await contract
    .deploy(txObject)
    .send({ from: '0xeb657c069c9b51cac4d587e7dc4cc26713912c5a', gas: 3000000 });

  const ca = instance.options.address;
  const deployed = new client.web3.eth.Contract(abi, ca);

  const data = await deployed.methods.value().call();
  console.log('before', data); // before Hello World
}

init();

 

코드를 실행시키고 마이닝을 했을때

콘솔로그에 Hello World가 찍힌다면 코드가 잘 작성된것이다.

 

이번엔 setter 함수를 실행 시키는 코드를 추가로 작성해보자.

 

async function init() {
  const instance = await contract
    .deploy(txObject)
    .send({ from: '0xeb657c069c9b51cac4d587e7dc4cc26713912c5a', gas: 3000000 });

  const ca = instance.options.address;
  const deployed = new client.web3.eth.Contract(abi, ca);

  const data = await deployed.methods.value().call();
  console.log('before', data);

  await deployed.methods.setValue('kongkong').send({
    from: '0xeb657c069c9b51cac4d587e7dc4cc26713912c5a',
  });
  const changed = await deployed.methods.value().call();
  console.log('after', changed);
}

init();

 

마찬가지로 코드를 실행시키고,

마이닝을 했을 때 처음엔 Hello World가 두번째는 kongkong가 찍힌다면 잘 된것이다.

 

3. Truffle

위의 모든 과정을 cli 명령어로 손쉽게 처리해주는 라이브러리가 있다.

 

저번 포스팅에 언급했던 truffle 이다.

우선은 설치부터 진행해보자

 

npm install -g truffle
mkdir truffle_sample && cd ./truffle_sample
truffle init

 

truffle init을 실행시키면

3개의 디렉토리와 하나의 파일이 생성되는데 각자 어떤 역할을 하는 지 알아보자.

 

  • contracts : solidity 코드 작성하는 공간
  • migrations : deploy
  • test : 배포된 애들을 실행시켜 보는 공간
  • build: (init 당시에는 생성되지 않지만 빌드 진행하면 생성됨) 컴파일 된 solidity 코드 내용이 담기는 공간
  • truffle-config.js : 설정 파일

이제 명령어들을 실행해보면서

이제껏 했던 모든 것들이 명령어 한줄로 해결되는 것들을 확인해보자...^^

 

3-0. config 설정

truffle을 사용하기전에 세팅을 조금 만져줘야 된다.


init으로 튀어나온 파일 중 config 파일에 들어가서 cmd+F를 검색기능을 켜고 networks를 검색해보자.
development 객체가 주석 처리가 되어있는데 주석 처리를 해제하고,
각자 네트워크 설정에 맞게 값을 지정해주자.

3-1. compile

기존에 작성했던 solidity 문서를 그대로 contracts 디렉토리에 복붙 해준뒤에 아래 명령어를 실행해주면

 

truffle compile

 

build 디렉토리가 생성되고 그안에 컴파일된 파일이 생성된다.
..ㅎ 뭔가 허무함.

 

3-2. contract 배포

이번엔 contract를 배포 해보자.

 

truffle migration
# 기존에 배포를 진행했다면 아래의 명령어를 실행하면 된다.
truffle migration --reset

 

migration을 하면 자동으로 트랜잭션을 발생시킨다.
트랜잭션을 발생시킨 주체는(계정 = EOA) 는 truffle-config.js 파일에서 설정 할 수 있다.


default 는 첫번째 계정임

트랜잭션이 발생했다면 txpool에 해당 내용이 담겼을 것이다.
마찬가지로 블록을 마이닝해서 contract를 배포해보자

 

3-3. contract 배포 확인

배포가 잘 되었는지 확인해보자.
아래의 명령어 truffle console 을 실행해보면,
터미널 모양이 살짝 바뀐다.

 

truffle console
truffle(development)>

 

방금 배포된 contract의 CA를 저장해두었다가 console 기능으로 해당 컨트랙트를 가져온다.
solidity 코드를 컴파일 한 파일의 이름으로 확인 할 수 있다.

 

truffle(development)> HelloWorld
# 객체 출력

 

HelloWorld를 입력했을 때 나오는 객체는 web3 라이브러리 + 가장 마지막에 배포된 contract의 내용 + truffle 기능 이 합쳐진 객체인듯...하다..
뇌피셜임..

 

truffle(development)> HelloWorld.address #CA 출력

 

위 명령어를 실행시키면 가장 최근에 배포된 contract 의 ca가 출력된다.

 

truffle(development)> HelloWorld.deployed().then((instance) => hello=instance)
hello #instance 출력

 

위의 명령어를 실행시키면 가장 최근에 배포된 contract 객체를 hello라는 변수에 담아주게 된다.

 

truffle(development)> hello.value()
# 'Hello World'

 

hello.value 를 호출 했을때 Hello World가 출력되는걸 토대로 확인할 수 있다.
이번에는 setValue를 실행해보자.

 

truffle(development)> hello.setValue('kongkong')

 

이게 스마트 컨트랙트를 실행시킨거다. 놀랍다.


스마트 컨트랙트가 실행되면,
실행 내용을 담은 트랜잭션이 발생된다.


당연히 마이닝을 해줘야 한다.

마이닝을 해주면
트랜잭션의 내용이 출력된다.

 

다시, value의 내용을 확인해보자.

 

truffle(development)> hello.value()
# kongkong

 

변경된 값이 출력되는 걸 확인 할 수 있다.

728x90
728x90

댓글