블록체인

[ethereum] truffle을 이용하여 스마트 컨트랙트 개발하기

멍개. 2022. 8. 28. 07:30

ruffle을 이용하여 스마트 컨트랙트를 개발하는 방법을 다룹니다.

 

셋업

$ npm install -g truffle

· 프로젝트 생성

$ mkdir dapp
$ cd dapp

$ truffle init

init 명령어를 사용하여 truffle 기반의 프로젝트를 생성할 수 있습니다.

$ tree
.
├── contracts
│   └── Migrations.sol
├── migrations
│   └── 1_initial_migration.js
├── test
└── truffle-config.js

3 directories, 3 files

▶ contracts

solidity 코드 관리

▶ migrations

스마트 컨트랙트 배포 관리

▶ test

테스트 코드 작성

▶ build

build는 컴파일 결과를 관리합니다.

프로젝트 생성시에는 없지만 compile을 하면 build/contracts 아래에 metadata.json를 생성합니다.

$ truffle compile

▶truffle-config.js

네트워크 연결정보 및 solidity compiler 버전 정보 등을 관리합니다.

/**
 * trufflesuite.com/docs/advanced/configuration
*/

// const HDWalletProvider = require('@truffle/hdwallet-provider');
// const fs = require('fs');
// const mnemonic = fs.readFileSync(".secret").toString().trim();

module.exports = {
  networks: {
    dev: {
     host: "127.0.0.1",
     port: 8545,       
     network_id: "*",
    },
  },
  mocha: {},
  compilers: {
    solc: {
      version: "0.8.10",
    }
  },
};
 

● 개발 프로세스

일반적으로 개발이라하면 다음과 같은 프로세스에 맞춰서 개발합니다.

1. 소스코드 작성

2. 컴파일 & 빌드

3. 배포

4. 개별적으로 실행해보기

5. 테스트 하기

truffle은 이런 일련의 과정을 코드 및 명령어 기반으로 수행할 수 있습니다.

· 소스코드 작성

소스코드는 contracts 아래에서 관리합니다.

파일명: contracts/Mung.sol

// SPDX-License-Identifier: UNLICENSED 
pragma solidity ^0.8.9; 

contract Mung {
    string public text; 

    constructor(string memory _text) { 
        text = _text;
    }
    
    function setText(string memory _text) public{
        text = _text;
    }

    function errorOccur(uint a) public pure returns (uint) {
        require(a != 0, "hello world error");
        return a;
    }
}

· 컴파일 및 빌드

$ truffle compile

Compiling your contracts...
===========================
> Compiling ./contracts/HelloWorld.sol
> Compiling ./contracts/Migrations.sol
> Compiling ./contracts/Mung.sol
> Artifacts written to /Users/jeongtaepark/Desktop/ethereum-truffle-sample/build/contracts
> Compiled successfully using:
   - solc: 0.8.10+commit.fc410830.Emscripten.clang

빌드를 성공적으로 완료하면 build/contracts/*.json를 생성합니다.

· 스마트컨트랙트 배포

truffle은 migrate 명령어만으로 아주 쉽게 배포를 할 수 있습니다.

배포에 대한 코드는 migrations 아래에서 관리합니다. 이 코드는 생성자 전달할 인자를 정의합니다.

파일명: migrations/2_Mung.js

여기서 한가지 중요한 점은 migrations는 다른 디렉터리와 다르게 "숫자_"를 prefix해야 합니다.

const Mung = artifacts.require("Mung");

module.exports = function (deployer) {
  deployer.deploy(Mung, "Hello Mung~");
};
$ truffle migrate --network [truffle-config에 명시한 network]

여기선 dev로 노드 정보를 작성했기 때문에 --network dev를 전달합니다.

$ truffle migrate --network dev
Compiling your contracts...
===========================
> Everything is up to date, there is nothing to compile.

> Something went wrong while attempting to connect to the network at http://127.0.0.1:8545. Check your network configuration.

Could not connect to your Ethereum client with the following parameters:
    - host       > 127.0.0.1
    - port       > 8545
    - network_id > *
Please check that your Ethereum client:
    - is running
    - is accepting RPC connections (i.e., "--rpc" or "--http" option is used in geth)
    - is accessible over the network
    - is properly configured in your Truffle configuration file (truffle-config.js)

Truffle v5.4.22 (core: 5.4.22)
Node v14.17.3

앗! 에러가!!!

할 시간에 에러를 읽으세요 제발좀...

해당 에러는 dev로 정의한 노드를 연결할 수 없다는 에러입니다. 당연히 연결을 못하죠. 노드가 로컬에 없으니

$ npm install -g ganache-cli
$ ganache-cli

ganache-cli를 이용하여 가상의 네트워크를 만듭니다.

다시 migrate하면 정상적으로 스마트 컨트랙트를 배포할 수 있습니다.

Compiling your contracts...
===========================
> Everything is up to date, there is nothing to compile.

Starting migrations...
======================
> Network name:    'dev'
> Network id:      1637999164665
> Block gas limit: 6721975 (0x6691b7)

2_Mung.js
=========

   Deploying 'Mung'
   ----------------
   > transaction hash:    0xb390ac6cc6cfe4b52220d0388e5b595fd1b79696de1ed1ced5b6fe373a6347ab
   > Blocks: 0            Seconds: 0
   > contract address:    0x680d78685dE52F50201bC4e5b1723Daf44Bc6449
   > block number:        5
   > block timestamp:     1637999206
   > account:             0x19258f77721790dB2cEBe89fdBE1f241BCA95202
   > balance:             99.97578014
   > gas used:            425863 (0x67f87)
   > gas price:           20 gwei
   > value sent:          0 ETH
   > total cost:          0.00851726 ETH


   > Saving migration to chain.
   > Saving artifacts
   -------------------------------------
   > Total cost:          0.00851726 ETH


Summary
=======
> Total deployments:   3
> Final cost:          0.02281934 ETH
 

· 실행

실행은 해당 컨트랙트의 함수 및 변수를 콘솔모드에서 호출할 수 있습니다.

$ truffle console --network dev
truffle(dev)>

truffle(network)>는 콘솔모드입니다.

▶ 배포한 컨트랙트 가져오기

truffle(dev)> contract = await Mung.deployed()

▶ text 변수 값 가져오기

truffle(dev)> contract.text.call()
'Hello Mung~'

▶ setText() 함수 호출

truffle(dev)> contract.setText( 'mung', { from: accounts[0] } )
{
  tx: '0x06e58ff5e6c6f3dd45d433549a0c0a299d88dea3d621c8f6b1ec5bc44984f8d2',
  receipt: {
    transactionHash: '0x06e58ff5e6c6f3dd45d433549a0c0a299d88dea3d621c8f6b1ec5bc44984f8d2',
    transactionIndex: 0,
    blockHash: '0x8bd35ce4b109100374e15f3bfe70995fa0e2f9a2ff534ec69fbc1b05e6c1ed15',
    blockNumber: 7,
    from: '0x19258f77721790db2cebe89fdbe1f241bca95202',
    to: '0x680d78685de52f50201bc4e5b1723daf44bc6449',
    gasUsed: 29574,
    cumulativeGasUsed: 29574,
    contractAddress: null,
    logs: [],
    status: true,
    logsBloom: '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
    rawLogs: []
  },
  logs: []
}

setText()는 트랜잭션을 발생하기 위해 마지막 인자로 트랜잭션을 발생할 주소를 전달합니다. 만약 {from: accounts[0]}이 없다면 기본으로 accounts[0]가 트랜잭션을 만듭니다.

▶ text 변수 값 가져오기

truffle(dev)> contract.text.call()
'mung'
truffle(dev)> contract.text()
'mung'

원하는 스타일로 작성합니다. 개인적으로 call()을 호출해서 사용하는 방법을 선호합니다.

▶ error를 일으키는 함수 정상 호출

truffle(dev)> contract.errorOccur(1, { from: accounts[0]})
BN { negative: 0, words: [ 1, <1 empty item> ], length: 1, red: null }

▶ error를 일으키는 함수 정상 호출

truffle(dev)> contract.errorOccur(0, { from: accounts[0]})
Uncaught:
Error: Returned error: VM Exception while processing transaction: revert hello world error

· 테스트하기

코드가 바뀔 때마다 앞의 과정을 반복적으로 한다면 아주 멍개같은 짓 입니다. 똑개는 절대 저렇게 테스트하지 않습니다.

테스트 코드는 test 아래에서관리합니다.

그 전에 라이브러리 하나를 설치하겠습니다.

$ npm install --save truffle-assertions

truffle-assertions은 revert가 발생하는 것을 테스트할 수 있습니다.

파일명: test/test_mung.js

const truffleAssert = require('truffle-assertions');
const Mung = artifacts.require("Mung");

contract("Mung", accounts => {
 // 각각의 테스트 케이스인 it을 실행할 때마다 before 실행하여 스마트 컨트랙트 객체를 가져옴
 before(async () => {
   this.instance = await Mung.deployed();
 })

 it("should be initialized with correct value", async () => {
   // 스마트 컨트랙트에 정의된 text() 함수 호출
   const text = await this.instance.text(); 
   assert.equal(text, "Hello Mung~", "Wrong initialized value!");
 });

 it("should change the text", async () => {
   const changedText = 'hoho';
   // 스마트 컨트랙트에 정의된 setText() 함수 호출
   // 마지막 객체는 트랜잭션 발생 정보를 가진다.
   await this.instance.setText(changedText, {from: accounts[0]});
   // 스마트 컨트랙트에 정의된 say() 함수 호출
   const text = await this.instance.text();
   assert.equal(text, changedText, "dose not change the value!");
 });

 it("should throw exception", async () => {
   // 스마트 컨트랙트에 정의된 errorOccur() 함수 호출
   await truffleAssert.reverts(
     this.instance.errorOccur(0, {from: accounts[0]}),
     "hello world error"
   )
 });

 it("should not throw exception", async () => {
   // 스마트 컨트랙트에 정의된 errorOccur() 함수 호출
   const rst = await this.instance.errorOccur(1, {from: accounts[0]});
   assert.equal(rst.words[0], 1, "ErrorOccur event not emitted!");
 });
})

$ truffle test --network dev
Using network 'dev'.


Compiling your contracts...
===========================
> Everything is up to date, there is nothing to compile.



  Contract: HelloWorld
    ✓ should be initialized with correct value
    ✓ should change the greeting (199ms)
    ✓ should throw exception (239ms)

  Contract: Mung
    ✓ should be initialized with correct value
    ✓ should change the text (190ms)
    ✓ should throw exception
    ✓ should not throw exception


  7 passing (838ms)

여기까지 진행했다면 다음과 같은 구조가 됩니다.

 
.
├── README.md
├── build
│   └── contracts
│       ├── Migrations.json
│       └── Mung.json
├── contracts
│   ├── Migrations.sol
│   └── Mung.sol
├── migrations
│   ├── 1_initial_migration.js
│   └── 2_Mung.js
├── package-lock.json
├── package.json
├── test
│   └── test_mung.js
└── truffle-config.js

5 directories, 11 files