Ethereum smart contract development and deployment
Post

Ethereum smart contract development and deployment

목표

설치한 ethereum에 smart contract를 개발하고 배포 해본다.

API 서버 구축

먼저 nodejs 기반으로 블록체인과 통신할 API 서버를 구축하자.
node, express 가 사전에 설치되어 있어야 한다.

1
2
npm i node
npm i express

기본 프로젝트를 생성한다. (express [프로젝트명])

1
express api

기본 모듈을 설치해 준다.

1
2
cd api
npm i

서버를 실행해본다.

1
npm run start

브라우저에 접속해본다. 기본 주소는 localhost:3000 이다.

스크립트 수정시 마다 nodejs 재기동이 번거로우므로 nodemon 을 설치하자.

1
npm i nodemon

package.json 에 아래 부분을 추가한다.

1
2
3
    "scripts": { "start": "node ./bin/www",  
                 "dev": "nodemon ./bin/www"  
    }

개발 시는 아래 명령어를 통해 nodejs 를 기동하여 사용한다.

1
npm run dev

스마트컨트랙트 작성 및 배포

스마트컨트랙트 작성 및 배포는 remix 를 사용한다.
프로젝트 remix 폴더 아래서 실행해준다.

1
2
cd remix
docker-compose up -d

접속은 http://127.0.0.1:8080 으로 할 수 있다.

이제 api 서버 내에 workspace 를 생성하고 접속해 보자.
api 폴더 아래 contracts 폴더를 생성한다.

1
2
cd api
mkdir contracts

폴더 아래 컨트랙트 파일을 추가한다. (예: DocumentFactory.sol)

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
// SPDX-License-Identifier: GPL-3.0

pragma solidity >=0.7.0 <0.9.0;

/**
 * @title Storage
 * @dev Store & retrieve value in a variable
 */
contract DocumentFactory {

    struct Document {
        string hash;   // short name (up to 32 bytes)
    }

    Document[] public documents;
    string[] public hashArray;

    /**
     * @dev Store value in variable
     * @param hash value to store
     */
    function store(string memory hash) public {
        documents.push(Document(hash));
    }

    /**
     * @dev Return value 
     * @return value of 'documents'
     */
    function retrieve() public view returns (Document[] memory){
        return documents;
    }

    function setHash(string memory hash) public {
        hashArray.push(hash);
    }

    function getHash() public view returns(string[] memory) {
        return hashArray;
    }
}

contracts 폴더에서 아래 명령을 실행한다.

1
remixd -s . --remix-ide http://127.0.0.1:8080

이제 remix 에서 해당 workspace에 접속이 가능하다.

좀 전에 추가한 스마트컨트랙트 파일을 컴파일 해준다.

이제 컴파일 된 스마트컨트랙트를 배포하자.

[ 왼쪽 탭 deploy 를 클릭 > Web3 Provider를 선택 >  설치한 블록체인에 접속 > 계정 선택, GAS LIMIT 설정, Contract 선택 > DEPLOY ]

스마트 컨트랙트가 정상 배포된 로그를 하단에서 확인 할 수 있다.

앞서 설치한 blockscout 을 통해 트랜잭션을 확인해 본다. 6788블록에 스마트컨트랙트가 담긴 것을 볼 수 있다.

WEB3 API 서비스 구축

이제 nodejs 서버를 통해 스마트컨트랙트과 통신을 해보자.
먼저 api 서버에 web3 모듈을 설치해준다. 

web3
이더리움 블록체인 및 스마트 컨트렉트와 상호 작용하는 데 사용되는 모듈 클래스

1
npm i web3

app.js 에 블록체인 path를 추가해주자.

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
 var createError = require('http-errors');  
 var express = require('express');
 
 var path = require('path');
 var cookieParser = require('cookie-parser');
 var logger = require('morgan');
 
 var indexRouter = require('./routes/index');
 var usersRouter = require('./routes/users');
 var blockRouter = require('./routes/block');
 
 var app = express();
 
 // view engine setup
 app.set('views', path.join(\_\_dirname, 'views'));
 app.set('view engine', 'jade');
 app.use(logger('dev'));
 app.use(express.json());
 app.use(express.urlencoded({ extended: false }));
 app.use(cookieParser());
 app.use(express.static(path.join(\_\_dirname, 'public')));
 app.use('/', indexRouter);
 app.use('/users', usersRouter);
 app.use('/block', blockRouter);
 
 // catch 404 and forward to error handler
 app.use(function(req, res, next) {
    next(createError(404));
 });
 
 // error handler
 app.use(function(err, req, res, next) {
 
 // set locals, only providing error in development
 res.locals.message = err.message;
 res.locals.error = req.app.get('env') === 'development' ? err : {};
 
 // render the error page
 res.status(err.status || 500);
 res.render('error');
 
 });
 
 module.exports = app;

routes 폴더 아래 block.js 파일을 추가해준다. 

1
2
3
4
5
6
7
8
9
10
var express = require('express');
var router = express.Router();
const Web3 = require('web3');

/* index page */
router.get('/', async function(req, res, next) {
    res.render('index', { title: 'blockchain api' });
});

module.exports = router;

상단에 기본 환경 변수를 설정 해준다.

  • ETHEREUM_URL : 설치된 이더리움 URL 주소
  • ETHEREUM_ACCOUNT : 사용할 계정 ID
  • CONTRACT_ADDR : 사용할 스마트컨트랙트 주소
1
2
3
4
const web3 = new Web3(process.env.ETHEREUM_URL || 'http://localhost:8545');
const default_account = process.env.ETHEREUM_ACCOUNT || '0x7d89dade013f1e8a9b5a22c9cc514d668be5000d';
const contractAddr = process.env.CONTRACT_ADDR || '0x32919F24c5F27c6CF8a8851f0962E81c540984aB';
const DocumentFactory = require('../contracts/artifacts/DocumentFactory.json');

이제 web3를 이용해 서비스를 추가하고 RestAPI 툴을 이용해 호출해보자. 여기서는 RestAPI 툴로 Postman을 사용하였다.

[유저 목록 가져오기]

1
2
3
4
5
6
7
8
9
10
/* 유저 목록 가져오기 */
router.post('/users', async function(req, res, next) {
    try {
        const result = await web3.eth.getAccounts();
        return res.json({ success: true, result: result })
    }catch(e) {
        console.log(e)
        return res.json({ success: false, message: 'fail' })
    }
});

[스마트컨트랙트에 데이터 저장하기]

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
/* 데이터 저장하기 */
router.post('/saveDocHash', async function(req, res, next) {

    const docHash = req.body.docHash
    if (!docHash) {
        return res.json({ success: false, message: "input value not enough!" })
    } 

    try {
        // 기본 계정 설정 및 UnLock
        web3.eth.defaultAccount = default_account;
        // await web3.eth.personal.unlockAccount(default_account, default_pwd, 600).then(console.log('Account unlocked!'));

        // Contract 지정
        var documentFactory = new web3.eth.Contract(DocumentFactory.abi, contractAddr, {
        from: default_account, // default from address
        gasPrice: '20000000000' // default gas price in wei, 20 gwei in this case 20000000000
        });

        // 저장
        const result = await documentFactory.methods.store(docHash).send();
        console.log(result);
        return res.json({ success: true, result: result })

    }catch(e) {
        console.log(e)
        return res.json({ success: false, message: 'fail' })
    }

});

[스마트컨트랙트 데이터 가져오기]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/* 데이터 가져오기 */
router.post('/docHashList', async function(req, res, next) {

    try {
        // Set Contract 
        var documentFactory = new web3.eth.Contract(DocumentFactory.abi, contractAddr);
        
        // 불러오기 (불러오기의 경우 계정 설정은 필요없다. 가스 소진 없음)
        const result = await documentFactory.methods.retrieve().call();
        console.log(result);

        return res.json({ success: true, result: result })

    }catch(e) {
        console.log(e)
        return res.json({ success: false, message: 'fail' })
    }

});

API 서버 도커로 배포하기

마지막으로 API 서버도 docker 이미지로 배포하자.
Dockerfile 을 추가해준다.

1
2
3
4
5
6
7
8
9
10
11
FROM node:16

RUN apt-get update || : && apt-get install python -y

WORKDIR /app
COPY package.json /app
RUN npm install
COPY . /app

# 실행 명령어
CMD ["npm", "run", "start"]

docker-compose.yml 파일을 추가해준다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
version: '3.3'
 
services:
  geth-api:
    build: # Dockerfile 빌드
      context: ./ # Dockerfile 빌드 경로
    container_name: geth-api # 컨테이너 명
    restart: "on-failure"
    # expose:
    #   - 5000 # 도커 내부적 포트
    ports:
      - 3003:3003
    env_file:
      -  ./common.env
    environment: # 환경변수 설정
      - NODE_ENV=production
      - CHOKIDAR_USEPOLLING=true

common.env 파일에 환경변수를 설정해준다.

1
2
3
4
ETHEREUM_URL=http://192.168.161.25:8545/
ETHEREUM_ACCOUNT=0x3c1bcAd30d646265E5aAFC7145295f84cA88e700
CONTRACT_ADDR=0x2D85432dDD83cbD1BD1dcbA7333A47258AAd5043
PORT=3000

API 서버를 실행해 준다.

1
docker-compose up -d