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

[Block-chain / 블록체인] smart contract event(스마트 컨트랙트 이벤트) 등록 및 Back-end 이용하여 contract 실행

by dev_kong 2022. 7. 14.
728x90
728x90

0. 목차

1. 개요

2. contract event 등록

3. Back-end 이용하여 contract 실행

1. 개요

이번 포스팅은 저번 포스팅에 이어서 작업을 할거다.


컨트랙트에 이벤트를 등록하고,
서명을 제외한 트랜잭션에 필요한 객체의 내용을 백엔드에서 만들어서 프론트에 응답으로 전해주면,
프론트에선 객체의 내용을 메타마스크에게 전달한다.


메타마스크는 트랜잭션 객체의 내용과 메타마스크에서 관리중인 프라이빗 키를 이용하여 서명을 만들고,
해당 내용을 이더리움네트워크로 던지는 역할을 한다.

 

백엔드는 MVC 패턴을 따라서 만들려고 한다.

2. Contract Event 등록

만약 지금 내가 만든 페이지에 여러명의 사람이 접속해있다고 치자 (A,B)

 

A라는 사람이 증가 버튼을 눌러서 트랜잭션을 발생시킨 뒤(컨트랙트 실행) 해당 트랜잭션을 담은 블록이 마이닝 된다면,

A가 보고 있는 화면에서는 숫자가 변경되지만 B가 보고 있는 화면 에서는 숫자가 변경 되지 않는다.

 

이러한 문제를 해결하기 위해 solidity에는 Event를 등록할 수 있다.

내부구조는 이제껏 많이 해본 websocket을 이용하여 구현 되어 있는 듯하다.

 

Counter.sol에 이벤트를 등록해보자

 

//SPDX-License-Identifier: MIT
pragma solidity ^0.8.15;

contract Counter {
    uint256 private _count;
    event Count(uint256 count);

    function current() public view returns (uint256) {
        return _count;
    }

    function increment() public {
        _count += 1;
        emit Count(_count);
    }

    function decrement() public {
        _count -= 1;
    }
}

 

매우 간단하다.

컨트랙트를 수정했기 때문에 컨트랙트 배포를 다시 진행해야 한다.

 

truffle을 이용해 간단하게 할수 있다.

 

truffle migration --reset

 

client의 Counter.json은 기존의 내용을 갖고있기 때문에,

다시 복붙 해주면 된다.

 

이거에 대한내용은 저번포스팅 에서 언급 했으니 넘어가겠다.

이제 react 코드를 수정해주자.

 

//Counter.jsx

  useEffect(() => {
    (async function () {
      if (deployed) return;

      const networkId = await web3.eth.net.getId();
      const { address: CA } = CounterContract.networks[networkId];
      const { abi } = CounterContract;

      const deployedContract = new web3.eth.Contract(abi, CA);

      const count = await deployedContract.methods.current().call();
            // 추가된 부분
      web3.eth
        .subscribe("logs", { address: CA }) //
        .on("data", console.log);

      setCount(count);
      setDeployed(deployedContract);
    })();
  });

 

요렇게 해주면

 

address: "0xCfCEa5a403A1A51C91A4D58f4e88f4B432024E5f"
blockHash: "0x4da98558de4ba3f854f0f9d50ac604bb9f6ecbac364f6fc3e502efc2846e7e3a"
blockNumber: 67
data: "0x0000000000000000000000000000000000000000000000000000000000000003"
id: "log_a40c27ad"
logIndex: 0
removed: false
topics: ['0xace32e4392fafee7f8245a5ae6a32722dc74442d018c52e460835648cbeeeba1']
transactionHash: "0x3025c37650f76362de0e9a69ebf73c929bff68c4c6ea54443b0f72cb2cadd754"
transactionIndex: 0
type: "mined"

 

요렇게 생긴 애가 뜬다.

그중에 data의 value가 카운트의 현재값이다.

 

근데 값을 읽기가 불-편하다.

또한 지금은 값을 하나만, 그리고 숫자를 전달하기에

data만 보고도 값의 예측이 가능하지만,

 

여러개의 값을 전달하게 된다면, 

이걸 쉽게 파싱해주는 기능이 web3에 존재한다.

 

      web3.eth
        .subscribe("logs", { address: CA }) //
        .on("data", (log) => {
          const params = [{ type: "uint256", name: "_count" }];
          const value = web3.eth.abi.decodeLog(params, log.data);
          console.log(value); // u {0: '5', __length__: 1, _count: '5'}
        });

 

요렇게 변경해주면 위와 같이 출력된다.

그럼 저 출력 되는 내용을 바탕으로 전체 코드를 조금 수정해주자.

 

import { useState, useEffect } from "react";
import CounterContract from "../contracts/Counter.json";

export const Counter = ({ web3, account }) => {
  const [count, setCount] = useState();
  const [deployed, setDeployed] = useState();

  const increment = async () => {
    await deployed.methods.increment().send({ from: account });
    // 위 트랜잭션이 발생되면, Event에 의해 값을 가져오고 값이 변경되므로, 현재값 불러오는 코드 삭제
  };

  const decrement = async () => {
    await deployed.methods.decrement().send({ from: account });
    // 위 트랜잭션이 발생되면, Event에 의해 값을 가져오고 값이 변경되므로, 현재값 불러오는 코드 삭제
  };

  useEffect(() => {
    (async function () {
      if (deployed) return;

      const networkId = await web3.eth.net.getId();
      const { address: CA } = CounterContract.networks[networkId];
      const { abi } = CounterContract;

      const deployedContract = new web3.eth.Contract(abi, CA);

      const count = await deployedContract.methods.current().call();

      web3.eth
        .subscribe("logs", { address: CA }) //
        .on("data", (log) => {
          const params = [{ type: "uint256", name: "_count" }];
          const value = web3.eth.abi.decodeLog(params, log.data);
            // 받아온 데이터로 state 변경
          setCount(value._count);
        });
            // 처음에 랜더될때 현재값 가져옴
      setCount(count);

      setDeployed(deployedContract);
    })();
  });

  return (
    <div>
      <h2>Counter : {count}</h2>
      <button onClick={increment}>증가</button>
      <button onClick={decrement}>감소</button>
    </div>
  );
};

 

그리고 저 logs는 transactionReceipt를 통해 확인 할 수 있다.

 

truffle console
truffle(development)> web3.eth.getTransactionReceipt('[txHash]')

{
  transactionHash: '0x296bbd1f8cbc9c89bc24e814caec7034c72c2d7a3683a04e804147b04f641907',
  transactionIndex: 0,
  blockHash: '0x46abef6f3ba950bca6fb552d5a0a00f35692873d4bb2a3aee3429411816a545a',
  blockNumber: 70,
  from: '0xd038f5ad3925fa9582a98981e88a99aeacbe4c0a',
  to: '0xcfcea5a403a1a51c91a4d58f4e88f4b432024e5f',
  gasUsed: '0x723a',
  cumulativeGasUsed: 29242,
  contractAddress: null,
  logs: [
    {
      logIndex: 0,
      transactionIndex: 0,
      transactionHash: '0x296bbd1f8cbc9c89bc24e814caec7034c72c2d7a3683a04e804147b04f641907',
      blockHash: '0x46abef6f3ba950bca6fb552d5a0a00f35692873d4bb2a3aee3429411816a545a',
      blockNumber: 70,
      address: '0xCfCEa5a403A1A51C91A4D58f4e88f4B432024E5f',
      data: '0x0000000000000000000000000000000000000000000000000000000000000006',
      topics: [Array],
      type: 'mined',
      removed: false,
      id: 'log_8f59eef3'
    }
  ],
  status: true,
  logsBloom: '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000010000000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000001000000000000000000000000000'
}

 

logs 배열에 log가 남아있다.

whale에다가 metamask를 깔아서 테스트를 돌려보니 아주 잘 동작하는 것을 확인할 수 있었다.

 

3. Back-end 이용하여 contract 실행

이를 통해 프론트 코드를 깔끔하게 정리 할 수 있고, 통신의 방향을 정리 할 수 있다.
프론트는 백엔드하고만 통신을 하고, 이더리움 네트워크와 통신을 하는것은 백엔드만 하게끔 설정을 해준다.


단, 서명이 필요한 부분은 프론트에서 메타마스크와 통신을 하는 식으로 구현을 할 수 있다.

우선 counter.jsx 부터 좀 정리를 해보자.


참고로 일단은 컨트랙트 실행에 대한 부분(increment, decrement) 부분만 수정을 할거다.
처음 화면이 랜더될 때 이더리움 네트워크와 통신하여 값을 가져오는 부분과,
이벤트를 등록한 부분은 잠시 내비두고,
다음에 수정을 해보려한다.

 

import { useState, useEffect } from "react";
import CounterContract from "../contracts/Counter.json";
import axios from "axios";

export const Counter = ({ web3, account }) => {
  const [count, setCount] = useState();
  const [deployed, setDeployed] = useState();

  const changeCount = (type) => async () => {
    const response = await axios.post("http://localhost:4000/api/counter", {
      from: account,
      type,
    });

    await web3.eth.sendTransaction(response.data);
  };

  useEffect(() => {
    (async function () {
      if (deployed) return;

      const networkId = await web3.eth.net.getId();
      const { address: CA } = CounterContract.networks[networkId];
      const { abi } = CounterContract;

      const deployedContract = new web3.eth.Contract(abi, CA);

      const count = await deployedContract.methods.current().call();

      web3.eth
        .subscribe("logs", { address: CA }) //
        .on("data", (log) => {
          const params = [{ type: "uint256", name: "_count" }];
          const value = web3.eth.abi.decodeLog(params, log.data);
          setCount(value._count);
        });

      setCount(count);

      setDeployed(deployedContract);
    })();
  });

  return (
    <div>
      <h2>Counter : {count}</h2>
      <button onClick={changeCount("increment")}>증가</button>
      <button onClick={changeCount("decrement")}>감소</button>
    </div>
  );
};

 

고차함수와 클로저를 이용해서 increment와 decrement를 하나로 합쳐버렸다.

back-end를 만들러 가자.

3. Back-end 이용하여 contract 실행

디렉토리 구조 부터 살펴보자

|- server
    |- models
    |- contracts
        |- counter.json
    |- controlls
        |- counter.controll.js
    |- services
        |- counter.services.js
    |- server.js
    |- connectWeb3.js

 

DB는 사용하지 않을 거기에 models는 우선 비워 두었다.
router도 안나눌거임

 

그리고 저번 포스팅에서 했듯이 truffle에 있는 Counter.json을 contract 디렉토리에 옮겨 주었다.

3-1. server.js

우선 server.js부터 만들자
express 를 이용할 건데 기본 세팅에 대한건 너무 많이 다뤘으니 pass

 

// server.js

const express = require("express");
const cors = require("cors");
const counterController = require("./controllers/counter.controller.js");

const app = express();

app.use(cors({ origin: true, credentials: true }));
app.use(express.json());

app.post("/api/counter", counterController.counter);

app.listen(4000);

 

간단하다.
그럼 이제 controll 을 만들러가자

 

3-2. controll

컨트롤 부분도 매우 간단하다.
요청과 응답에 대한 코드만 controll 쪽에 작성하고,
비즈니스 로직에 대한 부분은 service 쪽에 작성하기 때문이다.

 

// counter.controll.js

const TxObj = require("../services/counter.service.js");

const counter = async (req, res) => {
  const { from, type } = req.body;

  const txObj = await new TxObj().init(from, type);

  res.json(txObj);
};

module.exports = { counter };

 

허헣 이게 다다.

 

3-3. service

가장 핵심적인 부분이다.
결국 객체를 전달하므로 class를 이용해서 구현했다.

 

const CounterContract = require("../contract/Counter.json");
const SingletoneWeb3 = require("../connectWeb3.js");

const client = new SingletoneWeb3("http://127.0.0.1:8545");

class TxObj {
  async init(from, type) {
    this.nonce = await TxObj.getNonce(from);
    this.from = from;
    this.to = await TxObj.getCa();
    this.data = await TxObj.getData(type);
    return this;
  }

  static async getNonce(_account) {
    const nonce = await client.web3.eth.getTransactionCount(_account);
    return nonce;
  }

  static async getCa() {
    const networkId = await client.web3.eth.net.getId();
    const { address: to } = CounterContract.networks[networkId];
    return to;
  }

  static async getData(type) {
    const { abi } = CounterContract;
    const deployed = new client.web3.eth.Contract(abi, this.to);

    switch (type) {
      case "increment":
        return await deployed.methods.increment().encodeABI();

      case "decrement":
        return await deployed.methods.decrement().encodeABI();

      default:
        break;
    }
  }
}

module.exports = TxObj;

 

처음엔 class 내부에 constructor를 이용해서 프로퍼티의 밸류를 바로 static method를 이용해 할당해주려 했는데,
method들이 모두 비동기로 처리되는 함수이고, constructor에서는 async / await 사용이 불가능하여,
init이란 method를 만들고 init함수를 통해 instance의 프로퍼티를 만들어 주었다.

728x90
728x90

댓글