Web3.js 정리

반영환·2023년 6월 12일
0

BlockChainDev

목록 보기
9/11
post-thumbnail

Web3.js 정리

23-06-12 기준 정상 작동하는 코드입니다.

프로젝트 생성

express-starter

express 프로젝트 생성
express-starter-github

git remote add starter https://github.com/lopahn2/express-starter.git
git pull starter main
npm i

env

BLOCK_CHAIN_HTTP_PROVIDER = https://sepolia.infura.io/v3/<APIKEY>
SEND_ACCOUNT= 0xINPUT YOUR WALLET ADDRESS IN HERE
SEND_ACCOUNT_PK = INPUT YOUR WALLET PRIVATE KEY IN HERE

폴더 구조

build

컴파일 된 컨트랙트가 저장될 폴더
abibytecode가 적힌 json 파일이 저장된다.

contracts

빌드할 컨트랙트가 저장될 폴더.

controllers

Web3 모듈을 사용할 client.js, 스마트 컨트랙트 컴파일러 객체가 있는 compile.js, 컨트랙트를 작성할 템플릿 파일인 contractWriter.js가 들어있다.

client.js

import Web3 from "web3";

let instance;
class Client {
    constructor(_url) {
        if (instance) return instance;
        this.web3 = new Web3(_url);
        const account = this.web3.eth.accounts.privateKeyToAccount("0x" + process.env.SEND_ACCOUNT_PK);
        this.web3.eth.accounts.wallet.add(account);
        instance = this;
    }
}
 
export default Client;

web3 모듈을 사용할 클라이언트 객체이다.
싱글톤 패턴으로 작성됐다.

_url : 블록체인 IPFS URL을 넣어준다.

IPFSInterPlanetary File System의 약자로서, 분산형 파일 시스템에 데이터를 저장하고 인터넷으로 공유하기 위한 프로토콜이다. 냅스터, 토렌트(Torrent) 등 P2P 방식으로 대용량 파일과 데이터를 공유하기 위해 사용한다.

후에 컨트랙트 서명을 위해 자신의 지갑의 개인키로 계정을 만들어서 등록시켜준다.

compile.js

import solc from "solc";
import fs from 'fs-extra';
import path, { dirname } from "path";
import { fileURLToPath } from 'url';

import templateContract from './contractWriter.js';

const __fileName = fileURLToPath(import.meta.url);
const __dirname = dirname(__fileName);


class Contract {
  static compile(_filename) {
      const contractPath = path.join(__dirname, '../contracts', _filename + ".sol");
    
      fs.writeFileSync(contractPath, templateContract(_filename), "utf8");

      const data = JSON.stringify({
          language: 'Solidity',
          sources: {
              [_filename]: {
                  content: fs.readFileSync(contractPath, 'utf8'),
              },
          },
          settings: {
              outputSelection: {
                  '*': {
                      '*': ['*'],
                  },
              },
          },
      });

      const compiled = JSON.parse(solc.compile(data));
      console.log(compiled);
      return Contract.writeOutput(compiled); // [abi, bytecode]
  }

  static writeOutput(_compiled) {
      for (const contractFileName in _compiled.contracts) {
          const [contractName] = contractFileName.split('.');
          const contract = _compiled.contracts[contractFileName][contractName];

          const abi = contract.abi;
          const bytecode = contract.evm.bytecode.object;

          const obj = {
              abi,
              bytecode,
          };

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

          return [abi, bytecode];
      }
  }
}

export default Contract;
  • 생성하길 원하는 / 컴파일하기 원하는 컨트랙트의 경로를 받아온다.

  • 컨트랙트 라이터로 컨트랙트 내용을 작성한 뒤 파일을 작성한다.

  • 컨트랙트 생성용 DATA를 JSON으로 파싱해서 data 변수에 할당한다.

  • solc.compile(data) 로 컨트랙트를 컴파일한 뒤에 JS 객체로 파싱해 abi와 bytecode를 추출한다.

  • writeOutput에는 컴파일된 컨트랙트 데이터에서 abi와 bytecode를 추출한 뒤 JSON 파일 형태로 저장한 뒤 abi와 bytecode를 반환한다.

contractWriter.js

const templateContract = (contractTitle) => {
	 return `contract Content in here`
}

export default templateContract;

컨트랙트 온체인

import express from 'express';
import dotenv from 'dotenv';
import moment from 'moment-timezone'
moment.tz.setDefault('Asia/Seoul');
import { createRequire } from "module";

import { Chain, Common, Hardfork } from '@ethereumjs/common';
import { Transaction } from '@ethereumjs/tx';

import sqlCon from '../db/sqlCon.js'

import Contract from '../controllers/compile.js';
import Client from '../controllers/client.js';
import { makeGroupHashedID } from '../lib/hashing.js';


dotenv.config({ path: '../.env' });

const require = createRequire(import.meta.url);

const client = new Client(process.env.BLOCK_CHAIN_HTTP_PROVIDER);

const conn = sqlCon();
const router = express.Router();

const privateKey = Buffer.from(process.env.SEND_ACCOUNT_PK,'hex');
const regExp = /[\{\}\[\]\/?.,;:|\)*~`!^\-_+<>@\#$%&\\\=\(\'\"]/g;

/**
 * @Notice Write Smart Contract and Deploy on Chain Network
 * 
 * @Body
 *  group_id : Long group_id / 1
 *  title : String title / not included !, @, # ... / testGorupName
 * 
*/
router.post('/write', async function(req, res, next) {
  try {

    const contractInfo = req.body;

    if (regExp.test(contractInfo.title)) {
      return res.status(401).json(
        {
          message : "그룹 텍스트에 특수문자가 있는 경우 컨트랙트를 생성할 수 없습니다",
        }
      );
    }
    
    const contractTitle = contractInfo.title + "Contract";
    const hashedGroupInfo = await makeGroupHashedID(contractInfo.group_id, contractInfo.title);
    const contractWriteResult = {};

    console.log('transaction...compiling contract .....');
    const [abi, bytecode] = Contract.compile(contractTitle);

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

    const deploy = MyContract.deploy({
                    data: "0x" + bytecode,
                    from: process.env.SEND_ACCOUNT
                  }).encodeABI();
    
    client.web3.eth.getTransactionCount(process.env.SEND_ACCOUNT, (err, txCount) => {

      const txObject = {
        nonce:    client.web3.utils.toHex(txCount),
        gasLimit: 4000000,
        gasPrice: client.web3.utils.toHex(client.web3.utils.toWei('10', 'gwei')),
        data : deploy
      };
      
      const common = new Common({chain:Chain.Sepolia, hardfork: Hardfork.London});
      const tx = Transaction.fromTxData(txObject, { common });
      
      const signedTx = tx.sign(privateKey);
 
      const serializedTx = signedTx.serialize();

      const raw = '0x' + serializedTx.toString('hex');
      
      client.web3.eth.sendSignedTransaction(raw)
        .once('transactionHash', (hash) => {
          console.info('transactionHash', hash);
        })
        .once('receipt', async (receipt) => {
          const nowTime = moment().format("YYYY-M-D H:m:s");

          contractWriteResult.blockHash = receipt.blockHash;
          contractWriteResult.contractAddress = receipt.contractAddress;
          contractWriteResult.transactionHash = receipt.transactionHash;
          contractWriteResult.status = receipt.status;
          
          await conn.execute('INSERT INTO contracts VALUES (?,?,?,?,?,?,?)', [null, hashedGroupInfo.crypt, receipt.contractAddress, contractTitle,hashedGroupInfo.salt ,nowTime, nowTime]);
          await conn.execute('INSERT INTO contract_log VALUES (?,?,?,?)', [null, hashedGroupInfo.crypt, receipt.transactionHash, nowTime]);

          console.info('receipt', receipt);

          return res.status(201).json(
            {
              message : "컨트랙트 생성에 성공했습니다.",
              contractWriteResult
            }
          );
        }).on('error', (err) => {
          return res.status(401).json(
            {
              message : "컨트랙트 생성에 실패했습니다.",
              err
            }
          );
        });    
    });
  } catch(e) {
    console.log(e);
    return res.status(401).json(
      {
        message : "예상치 못한 에러가 발생했습니다.",
        error : e
      }
    );
  }
});

컨트랙트 컴파일

const [abi, bytecode] = Contract.compile(contractTitle);

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

const deploy = MyContract.deploy({
                  data: "0x" + bytecode,
                  from: process.env.SEND_ACCOUNT
                }).encodeABI();

컨트랙트 txObject

const txObject = {
        nonce:    client.web3.utils.toHex(txCount),
        gasLimit: 4000000,
        gasPrice: client.web3.utils.toHex(client.web3.utils.toWei('10', 'gwei')),
        data : deploy
      };
  • Gas Limit : 사용할 가스에 대한 예측치( 너무 높으면 거절 당할 수 있음)

  • Gas Price : Gas Limit 에서 1 Gas 당 가격, 높을수록 빨리 실행된다.

  • 총 비용 : Gas Limit 중 실제 사용량 * Gas Price

  • Block Gas Limit : 한 블록에 넣을 수 있는 Gas Limit 의 총합

컨트랙트 data sign

const common = new Common({chain:Chain.Sepolia, hardfork: Hardfork.London});
const tx = Transaction.fromTxData(txObject, { common });

const signedTx = tx.sign(privateKey);

const serializedTx = signedTx.serialize();

const raw = '0x' + serializedTx.toString('hex');

일반적인 블록체인 네트워크의 정보를 common 객체를 통해 Transaction 진행시 전달해준다.

트렌젝션에 서명은 발생자의 privateKey를 이용해 진행하며 privateKey는 버퍼로 만들어준다.

const privateKey = Buffer.from(process.env.SEND_ACCOUNT_PK,'hex');

컨트랙트 온체인

client.web3.eth.sendSignedTransaction(raw)
        .once('transactionHash', (hash) => {
          console.info('transactionHash', hash);
        })
        .once('receipt', async (receipt) => {
          const nowTime = moment().format("YYYY-M-D H:m:s");

          contractWriteResult.blockHash = receipt.blockHash;
          contractWriteResult.contractAddress = receipt.contractAddress;
          contractWriteResult.transactionHash = receipt.transactionHash;
          contractWriteResult.status = receipt.status;
          
          await conn.execute('INSERT INTO contracts VALUES (?,?,?,?,?,?,?)', [null, hashedGroupInfo.crypt, receipt.contractAddress, contractTitle,hashedGroupInfo.salt ,nowTime, nowTime]);
          await conn.execute('INSERT INTO contract_log VALUES (?,?,?,?)', [null, hashedGroupInfo.crypt, receipt.transactionHash, nowTime]);

          console.info('receipt', receipt);

          return res.status(201).json(
            {
              message : "컨트랙트 생성에 성공했습니다.",
              contractWriteResult
            }
          );
        }).on('error', (err) => {
          return res.status(401).json(
            {
              message : "컨트랙트 생성에 실패했습니다.",
              err
            }
          );
        });    

컨트랙트 작성 ( 트랜젝션 발생 )

// ... 위 파일 내용과 동일 경로
/**
 * @Notice setStudyGroupContracts Call Router
 * 
 * @Param groupId : Long group_id
 * @Param title : String title / Not included !,@,#, ... 
 * 
 * @body
 *  leaderId : String leaderId / admin
 *  capacity : Int capacity / 4
 *  groupDepositPerPerson : Int groupDepositPerPerson / 10000
 *  groupPeriod : Int groupPeriod / 30 (days)
 */
router.post('/:groupId/:title', async (req, res, next) => {
  try {

    const params = req.params;
    const body = req.body;

    if (regExp.test(params.title)) {
      return res.status(401).json(
        {
          message : "그룹 텍스트에 특수문자가 있는 경우는 없습니다.",
        }
      );
    }

    const hashedGroupInfo = await makeGroupHashedID(params.groupId, params.title);

    const groupCreateInfo = {

      leaderId : body.leaderId,
      groupId : hashedGroupInfo.crypt,
      capacity : body.capacity,
      groupDepositPerPerson : body.groupDepositPerPerson,
      groupPeriod : body.groupPeriod

    }
    
    const [contractInfo, ] = await conn.execute('select * from contracts where hashed_group_id = ?',[hashedGroupInfo.crypt]);
    const CA = contractInfo[0].contract_address;
    const encodedContract = require(`../build/${params.title}Contract.json`);
    
    const deployedContract = new client.web3.eth.Contract(encodedContract.abi, CA);
    
    deployedContract.methods
      .setStudyGroupContracts(
        groupCreateInfo.leaderId,
        groupCreateInfo.groupId,
        groupCreateInfo.capacity,
        groupCreateInfo.groupDepositPerPerson,
        groupCreateInfo.groupPeriod
      ).send({ 
        from : process.env.SEND_ACCOUNT,
        gas: 4000000
      })
      .on("receipt", (receipt) => {
        return res.status(201).json(
          {
            message : "컨트랙트 작성에 성공했습니다.",
            receipt
          }
        );
      });

    

  } catch (error) {
    console.log(error);
    return res.status(400).json(
      {
        message : "존재하지 않는 그룹의 스마트 컨트랙트 입니다. ID는 Long type 식별자를 썼는지, Title에 오타가 없는지 확인하세요.",
        error : error.message
      }
    );
  }
});

온체인된 컨트랙트 불러오기

const [contractInfo, ] = await conn.execute('select * from contracts where hashed_group_id = ?',[hashedGroupInfo.crypt]);
const CA = contractInfo[0].contract_address;
const encodedContract = require(`../build/${params.title}Contract.json`);

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

DB에 저장된 컨트랙트의 주소와 build 폴더에 저장된 json 파일에서 컨트랙트의 abi를 통해서 온체인된 컨트랙트를 불러올 수 있다.

set 함수 사용하기

deployedContract.methods
      .setStudyGroupContracts(
        groupCreateInfo.leaderId,
        groupCreateInfo.groupId,
        groupCreateInfo.capacity,
        groupCreateInfo.groupDepositPerPerson,
        groupCreateInfo.groupPeriod
      ).send({ 
        from : process.env.SEND_ACCOUNT,
        gas: 4000000
      })
      .on("receipt", (receipt) => {
        return res.status(201).json(
          {
            message : "컨트랙트 작성에 성공했습니다.",
            receipt
          }
        );
      });

블록체인에 온체인된 컨트랙트 내용을 작성할 수 있다.

여기서 client 객체에 우리 지갑의 개인키를 등록시키지 않으면 에러가 발생한다.

컨트랙트 내용 읽기

// ... 위 파일 내용과 동일 경로
/**
 * @Notice callContractDetail Call Router
 * 
 * @Param groupId : Long group_id
 * @Param title : String title / Not included !,@,#, ... 
 * 
 */
router.get('/callContract/:groupId/:title', async (req, res, next) => {
  try {
    
    const params = req.params;

    if (regExp.test(params.title)) {
      return res.status(401).json(
        {
          message : "그룹 텍스트에 특수문자가 있는 경우는 없습니다.",
        }
      );
    }

    const hashedGroupInfo = await makeGroupHashedID(params.groupId, params.title);
    
    const [contractInfo, ] = await conn.execute('select * from contracts where hashed_group_id = ?',[hashedGroupInfo.crypt]);
    const CA = contractInfo[0].contract_address;
    const encodedContract = require(`../build/${params.title}Contract.json`);
    
    const deployedContract = new client.web3.eth.Contract(encodedContract.abi, CA);
    
    deployedContract.methods
      .callContractDetail()
      .call({
        from : process.env.SEND_ACCOUNT,
        gas : 4000000
      })
      .then(result =>console.info(result))
      .catch(err => console.log(err));


    return res.status(201).json(
      {
        message : "컨트랙트 읽기에 성공했습니다.",
        hashedGroupInfo
      }
    );

  } catch (error) {
    console.log(error);
    return res.status(400).json(
      {
        message : "존재하지 않는 그룹의 스마트 컨트랙트 입니다. ID는 Long type 식별자를 썼는지, Title에 오타가 없는지 확인하세요.",
        error : error.message
      }
    );
  }
});

출처

web3.js

profile
최고의 오늘을 꿈꾸는 개발자

0개의 댓글

관련 채용 정보