스마트 컨트랙트(Smart Contract)에 대한 서명(VRS) 코딩 실습

2021. 10. 15. 16:43Blockchain/Truffle

실습

  1. truffle unbox react
  2. ganache 실행 후 metamask 연결
  3. truffle-config.js 내용 수정 // 이전과 같이
  4. /contracts/SimpleStorage.sol 코드 수정
    // SPDX-License-Identifier: MIT
    pragma solidity >=0.4.21 <0.7.0;
    
    contract SimpleStorage {
      uint storedData;
      event Change(string message, uint newVal); // 추가
    
      function set(uint x) public {
        storedData = x;
        emit Change('set', x); // 추가
      }
    
      function get() public view returns (uint) {
        return storedData;
      }
    }​
  5. truffle compile
  6. truffle migrate
  7. /client/src/App.js 코드 수정
    import React, { useState } from "react";
    import SimpleStorageContract from "./contracts/SimpleStorage.json";
    import getWeb3 from "./getWeb3";
    
    import "./App.css";
    
    const App = () => {
        const [value, setValue] = useState(0);
        const [storage, setStorage] = useState(0);
        const [loading, setLoading] = useState(false);
    
        // 직접 서명
        const send = () => {
    
        }
    
        // DB(Back-end) 거치고 서명
        const sendAPI = () => {
            
        }
    
        // DB 서명
        const sendTx = () => {
            
        }
    
        const handleChange = e => {
            const val = e.target.value;
            setValue(val);
        }
    
        return (
            <div style={{padding:"20px"}}>
                <input type="text" value={value} onChange={handleChange} />
                <div>
                    <button onClick={send}>직접 서명</button>
                    <button onClick={sendAPI}>DB 거치고 서명</button>
                    <button onClick={sendTx}>DB 서명</button>
                </div>
                <div>
                    {loading ? '로딩 중' : storage}
                </div>
            </div>
        );
    }
    
    export default App;​
    화면 구성
     
  8. npm install @truffle/contract // 서버 종료 후 패키지 설치
  9. 로딩 상태 변수로 선언하여 처리, 설치 한 패키지 가져오기, Web3와 contract 연결
    (이전 포스트 참조, 링크 : https://hwan91.tistory.com/12)
    import React, { useEffect, useState } from "react";
    import SimpleStorageContract from "./contracts/SimpleStorage.json";
    import getWeb3 from "./getWeb3";
    
    import "./App.css";
    
    const App = () => {
        const [value, setValue] = useState(0);
        const [storage, setStorage] = useState(0);
        const [loading, setLoading] = useState(false);
    
        const handleResult = () => {
            setStorage(value);
            setLoading(prev => !prev);
        }
    
        // 직접 서명
        const send = () => {
            if (value > 0) {
                setLoading(prev => !prev);
                handleResult(); // 비동기적 처리
            }
        }
    
        // DB(Back-end) 거치고 서명
        const sendAPI = () => {
            if (value > 0) {
                setLoading(prev => !prev);
                handleResult(); // 비동기적 처리
            }
        }
    
        // DB 서명
        const sendTx = () => {
            if (value > 0) {
                setLoading(prev => !prev);
                handleResult(); // 비동기적 처리
            }
        }
    
        const handleChange = e => {
            const val = e.target.value;
            setValue(val);
        }
    
        const init = async () => {
            const contract = require('@truffle/contract');
            const web3 = await getWeb3();
            const [account] = await web3.eth.getAccounts();
            // const networkId = await web3.eth.net.getId(); // network id값 가져오기, 참고
    
            let simpleStorage = contract(SimpleStorageContract);
            simpleStorage.setProvider(web3.currentProvider);
    
            const instance = await simpleStorage.deployed();
    
            console.log(web3);
            console.log(account);
            console.log(instance);
        }
    
        useEffect(() => {
            init();
        }, []);
    
        return (
            <div style={{padding:"20px"}}>
                <input type="text" value={value} onChange={handleChange} />
                <div>
                    <button onClick={send}>직접 서명</button>
                    <button onClick={sendAPI}>DB 거치고 서명</button>
                    <button onClick={sendTx}>DB 서명</button>
                </div>
                <div>
                    {loading ? '로딩 중' : storage}
                </div>
            </div>
        );
    }
    
    export default App;​
    차례대로 console.log(web3, account, instance);
  10. reducer와 dispatch를 사용하여 상태 관리
    import React, { useEffect, useReducer, useState } from "react";
    import SimpleStorageContract from "./contracts/SimpleStorage.json";
    import getWeb3 from "./getWeb3";
    
    import "./App.css";
    
    const reducer = (state, action) => {
        switch (action.type) {
            case "INIT":
                let { web3, instance, account } = action;
                return {
                    ...state,
                    web3,
                    instance,
                    account,
                }
        }
    }
    
    const INIT_ACTIONS = (web3, instance, account) => {
        return {
            type: 'INIT',
            web3,
            instance,
            account,
        }
    }
    
    const App = () => {
        const initialState = {
            web3: null,
            instance: null,
            account: null,
        }
    
        const [value, setValue] = useState(0);
        const [storage, setStorage] = useState(0);
        const [loading, setLoading] = useState(false);
        const [state, dispatch] = useReducer(reducer, initialState);
    
        const handleResult = () => {
            setStorage(value);
            setLoading(prev => !prev);
        }
    
        // 직접 서명
        const send = () => {
            if (value > 0) {
                setLoading(prev => !prev);
                handleResult(); // 비동기적 처리
            }
        }
    
        // DB(Back-end) 거치고 서명
        const sendAPI = () => {
            if (value > 0) {
                setLoading(prev => !prev);
                handleResult(); // 비동기적 처리
            }
        }
    
        // DB 서명
        const sendTx = () => {
            if (value > 0) {
                setLoading(prev => !prev);
                handleResult(); // 비동기적 처리
            }
        }
    
        const handleChange = e => {
            const val = e.target.value;
            setValue(val);
        }
    
        const init = async () => {
            const contract = require('@truffle/contract');
            const web3 = await getWeb3();
            const [account] = await web3.eth.getAccounts();
            // const networkId = await web3.eth.net.getId(); // network id값 가져오기, 참고
    
            let simpleStorage = contract(SimpleStorageContract);
            simpleStorage.setProvider(web3.currentProvider);
    
            const instance = await simpleStorage.deployed();
    
            // console.log(web3);
            // console.log(account);
            // console.log(instance);
    
            dispatch(INIT_ACTIONS(web3, instance, account));
        }
    
        useEffect(() => {
            init();
        }, []);
    
        return (
            <div style={{padding:"20px"}}>
                <input type="text" value={value} onChange={handleChange} />
                <div>
                    <button onClick={send}>직접 서명</button>
                    <button onClick={sendAPI}>DB 거치고 서명</button>
                    <button onClick={sendTx}>DB 서명</button>
                </div>
                <div>
                    {loading ? '로딩 중' : storage}
                </div>
            </div>
        );
    }
    
    export default App;​
    상태값 확인
  11. 직접 서명에 대한 코드 작성
    콘솔 출력 테스트 // 5000 입력, 직접 서명 버튼 클릭 후 MetaMaskNontification에서 확인
    import React, { useEffect, useReducer, useState } from "react";
    import SimpleStorageContract from "./contracts/SimpleStorage.json";
    import getWeb3 from "./getWeb3";
    
    import "./App.css";
    
    const reducer = (state, action) => {
        switch (action.type) {
            case "INIT":
                let { web3, instance, account } = action;
                return {
                    ...state,
                    web3,
                    instance,
                    account,
                }
        }
    }
    
    const INIT_ACTIONS = (web3, instance, account) => {
        return {
            type: 'INIT',
            web3,
            instance,
            account,
        }
    }
    
    const App = () => {
        const initialState = {
            web3: null,
            instance: null,
            account: null,
        }
    
        const [value, setValue] = useState(0);
        const [storage, setStorage] = useState(0);
        const [loading, setLoading] = useState(false);
        const [state, dispatch] = useReducer(reducer, initialState);
    
        const handleResult = (log, web3) => {
            // data : 0x0000000000000000000
            // decodeLog의 인자값 >> 1: 데이터 형식(params), 2: log.data
            const params = [ // .sol 파일에 작성했던 event Change의 인자값 내용과 동일
                {
                    type: 'string',
                    name: 'message',
                },
                {
                    type: 'uint256',
                    name: 'newVal',
                },
            ];
            const returnValues = web3.eth.abi.decodeLog(params, log.data);
            console.log(returnValues);
    
            // case가 많을 경우 switch문으로 작성
            if (returnValues.message == 'set') {
                setStorage(value);
                setLoading(prev => !prev);
            }
        }
    
        // 직접 서명
        const send = async () => {
            const { account, instance } = state;
    
            if (value > 0) {
                setLoading(prev => !prev);
                await instance.set(value, { from: account });
                // handleResult(); // 비동기적 처리
            }
        }
    
        // DB(Back-end) 거치고 서명
        const sendAPI = () => {
            if (value > 0) {
                setLoading(prev => !prev);
                // handleResult(); // 비동기적 처리
            }
        }
    
        // DB 서명
        const sendTx = () => {
            if (value > 0) {
                setLoading(prev => !prev);
                // handleResult(); // 비동기적 처리
            }
        }
    
        const handleChange = e => {
            const val = e.target.value;
            setValue(val);
        }
    
        const init = async () => {
            const contract = require('@truffle/contract');
            const web3 = await getWeb3();
            const [account] = await web3.eth.getAccounts();
            // const networkId = await web3.eth.net.getId(); // network id값 가져오기, 참고
    
            let simpleStorage = contract(SimpleStorageContract);
            simpleStorage.setProvider(web3.currentProvider);
    
            const instance = await simpleStorage.deployed();
    
            // console.log(web3);
            // console.log(account);
            // console.log(instance);
    
            dispatch(INIT_ACTIONS(web3, instance, account));
    
            // websocket 사용과 비슷
            web3.eth.subscribe('logs', { address: instance.address }) // 배포가 된, contract 계정의 address
            .on('data', log => {
                console.log(log);
                handleResult(log, web3);
            })
            .on('error', err => {
                console.log(err);
            })
        }
    
        useEffect(() => {
            init();
        }, []);
    
        return (
            <div style={{padding:"20px"}}>
                <input type="text" value={value} onChange={handleChange} />
                <div>
                    <button onClick={send}>직접 서명</button>
                    <button onClick={sendAPI}>DB 거치고 서명</button>
                    <button onClick={sendTx}>DB 서명</button>
                </div>
                <div>
                    {loading ? '로딩 중' : storage}
                </div>
            </div>
        );
    }
    
    export default App;​
    console.log(log);
    console.log(returnValues);
      
  12.  
  13. npm install axios // 다음 작업에 필요한 패키지 설치 후 import로 가져오기
  14. root 디렉토리에서 npm init
    root 디렉토리에서 server 폴더 생성, 그 안에 아래와 같은 구조로 폴더 및 파일 생성
  15. /server/server.js 코드 작성
    const express = require('express');
    const app = express();
    const bodyParser = require('body-parser');
    const cors = require('cors');
    const router = require('./routes');
    
    app.use(cors());
    app.use(bodyParser.urlencoded({ extended: false }));
    app.use(bodyParser.json());
    
    app.use('/', router);
    
    app.listen(3001, () => {
        console.log(`server on port 3001`);
    });​
  16. /server/routes/index.js 코드 작성
    const express = require('express');
    const router = express.Router();
    const rpc = require('./rpc');
    
    router.use('/rpc', rpc);
    
    module.exports = router;​
  17. /server/routes/rpc/index.js 코드 작성
    const express = require('express');
    const router = express.Router();
    const controller = require('./rpc_controller');
    
    router.post('/set', controller.set);
    router.post('/setTx', controller.setTx);
    
    module.exports = router;​
  18. /server/routes/rpc/rpc_controller.js 코드 작성
    const Web3 = require('web3');
    const web3 = new Web3('http://localhost:8545');
    const { abi } = require('../../../client/src/contracts/SimpleStorage.json');
    const { address } = require('../../../client/src/contracts/SimpleStorage.json').networks['5777'];
    const ethTx = require('ethereumjs-tx').Transaction;
    
    // Back-end 거치고 서명
    const set = async (req, res) => {
        const { from, val } = req.body;
    
        try {
            const contract = await new web3.eth.Contract(abi, address);
            const data = await contract.methods.set(val).encodeABI();
    
            console.log('data : ', data);
    
            let txObject = { // transaction == tx
                from,
                to: address, // contract address >> /client/src/contracts/SimpleStorage.json의 networks 객체안에 5777 안에 address.. 길어서 전역 변수로 뺌
                data, // 배포한 set 메서드를 인자값을 넣은 set(10)으로 바꿔줘야함 >> set 메서드 사용을 위해 web3 사용
                gasLimit: web3.utils.toHex(30000), // hex(16진수)로 변환
                gasPrice: web3.utils.toHex(web3.utils.toWei('20', 'gwei')), // wei 단위로 작성 후 hex로 변환
            }
    
            res.json({
                // msg: 'hello set',
                success: true,
                raxTx: txObject,
            });
        } catch (e) {
            console.log(e);
            res.json({
                success: false,
            });
        }
    }
    
    // Back-end 서명
    const setTx = async (req, res) => {
        const { from, val } = req.body;
        const privateKey = Buffer.from('fae7534c013c508e384b4d912bf258ea0ebdd18a636eef5a736f975b55c41d90', 'hex'); // '가나슈 0번 인덱스의 privatekey, + hex로 반환'
        
        try {
            const instance = await new web3.eth.Contract(abi, address);
            const data = await instance.methods.set(val).encodeABI();
            const txCount = await web3.eth.getTransactionCount(from);
    
            console.log('data : ', data);
    
            let txObject = {
                nonce: web3.utils.toHex(txCount),
                from,
                to: address,
                data,
                gasLimit: web3.utils.toHex(30000),
                gasPrice: web3.utils.toHex(web3.utils.toWei('20', 'gwei')),
            }
    
            // 서명 시 개인키가 필요, 따라서 비밀키(privateKey)가 필요
            const tx = new ethTx(txObject);
            console.log(tx);
    
            tx.sign(privateKey);
            const serializedTx = tx.serialize();
            console.log(serializedTx.toString('hex'));
    
            const txhash = await web3.eth.sendSignedTransaction('0x' + serializedTx.toString('hex'));
    
            res.json({
                // msg: 'hello setTx',
                success: true,
                txhash,
            });
        } catch (e) {
            console.log(e);
            res.json({
                success: false,
            });
        }
    }
    
    module.exports = {
        set,
        setTx,
    }​
    Postman 사용, POST /rpc/set, 빨간 네모 안에 JSON 설정을 하지 않아 console.log(data)에 대한 터미널 에러 발생 (아래)
    에러 문구
    /rpc/set에 대한 console.log(data); // 정상 출력
    /rpc/set에 대한 Postman 결과값
    POST /rpc/setTx
    /rpc/setTx에 대한 console.log(tx);
    rpc/setTx에 대한 console.log(serializedTx
    /rpc/setTx에 대한 Post맨 결과값