BlockChain

KAS를 사용해서 간단한 서버-클라이언트 프로젝트 만들기

es_0409 2022. 4. 6. 17:45

이번에는 KAS를 사용해서 간단한 서버-클라이언트를 작성해보고자 한다.

기능은 총 3가지로, 아래와 같이 구현하고자 했다.

  • 최신 블록 넘버 받아오기
  • EOA 계정을 입력해서 계정 정보 받아오기
  • KAS basic 저장소에 신규 계정 생성하기

https://github.com/EunsuGoh/SimpleEtherscanWeb3

 

GitHub - EunsuGoh/SimpleEtherscanWeb3

Contribute to EunsuGoh/SimpleEtherscanWeb3 development by creating an account on GitHub.

github.com

태초에 web3가 있었다..

위 레포지토리는 비슷한 느낌의 web3 기반 프로젝트이다. 위 프로젝트에서 web3 대신, caver-js를 사용하여 KAS 프로젝트를 새로 만들어보고자 하였다.


사용한 모듈은 아래와 같다.

// package.json 
 ...
 "dependencies": {
    "@testing-library/jest-dom": "^5.16.4",
    "@testing-library/react": "^12.1.4",
    "@testing-library/user-event": "^13.5.0",
    "axios": "^0.26.1",
    "caver-js-ext-kas": "^1.9.0",
    "cors": "^2.8.5",
    "express": "^4.17.3",
    "nodemon": "^2.0.15",
    "react": "^18.0.0",
    "react-dom": "^18.0.0",
    "react-scripts": "5.0.0",
    "web-vitals": "^2.1.4"
  }
  ...

개발 편의성을 위해 nodemon을 사용했으며, express, react, axios(fetch 대신), caver-js-ext-kas를 사용했다.


Server Side

/my-kas-basic-app/src/server.js

const config = require("../config.js");
const caver = config.caver_ext;
const express = require("express");
const cors = require("cors");
const app = express();
const port = 8080;

app.use(express.json());
app.use(cors());

async function getLatestBlockNumber() {
  const blockNumber = await caver.rpc.klay.getBlockNumber();
  return blockNumber;
}

async function getEoaInfo(account) {
  const eoaInfo = await caver.rpc.klay.getAccount(account);
  return eoaInfo;
}

async function makeAccount() {
  const accountInfo = await caver.kas.wallet.createAccount();
  return accountInfo;
}
app.get("/latestblock", (req, res) => {
  try {
    getLatestBlockNumber().then((result) => {
      res.status(200).json(result);
    });
  } catch (e) {
    console.log(e);
    res.status(400).send("Error");
  }
});

app.post("/eoainfo", (req, res) => {
  try {
    let account = req.body.account;
    if (!account) {
      res.status(400).send("Invalid Account");
    } else {
      getEoaInfo(account).then((result) => {
        console.log(result);
        res.status(200).json(result);
      });
    }
  } catch (e) {
    console.log(e);
    res.status(400).send("Error");
  }
});
app.get("/createaccount", (req, res) => {
  try {
    makeAccount().then((result) => {
      console.log(result);
      res.status(200).json(result);
    });
  } catch (e) {
    console.log(e);
    return e;
  }
});

app.listen(port, () => {
  console.log(`server is listening on ${port}...`);
});

전체 코드는 위와 같다.


config.js

const accessKeyId = "YOUR ACCESS KEY";
const secretAccessKey = "YOUR SECREST ACCESS KEY";
const authorization ="YOUR AUTHORIZATION CODE";
const Caver_ext = require("caver-js-ext-kas");
const caver_ext = new Caver_ext(1001, accessKeyId, secretAccessKey);

const Caver = require("caver-js");
// baobab = 1001(chain id)
const caver = new Caver(1001, accessKeyId, secretAccessKey);

module.exports = {
  accessKeyId,
  secretAccessKey,
  authorization,
  caver_ext,
  caver,
};

귀찮게 매번 caver에 연결해서 쓰지 않기 위해 따로 config.js에 설정을 저장해서 export하여 사용했다.

무료 api를 다 뜯기는 불상사를 방지하기 위해 gitignore에 등록해 주었다.

KAS 공식 문서에 따르면 baobab 테스트넷의 체인아이디는 1001이므로, 위와 같이 작성해준다.

 

cors

cors 에러를 방지하기 위해 app.use(cors())로 모든 크로스 오리진에 대한 요청을 허용해준다.

파라미터, 쿼리 문으로 클라이언트 요청을 처리할 수도 있지만 나는 post요청으로 처리했다.

이런 경우, body-parser가 없으면 req.body를 읽어오지 못하는 오류가 생길 수 있다.

따라서 app.use(express.json())으로 요청의 바디 데이터를 파싱해줘야한다.

(원래는 바디파서를 따로 모듈로 받아야했지만, express 내장 모듈로 변경되어 저렇게만 만들어줘도 귀찮게 코드를 작성하지 않아도 된다..

 


서버 함수

// 클라이언트에서 요청하는 데이터 전달을 위한 함수들

// 1 . 블록 넘버를 받아오는 함수
async function getLatestBlockNumber() {
  //  caver-js를 사용하여 KAS api를 요청한다.
  const blockNumber = await caver.rpc.klay.getBlockNumber();
  return blockNumber;
}

// 2 . 외부 계정 정보를 받아오는 함수
async function getEoaInfo(account) {
//  caver-js를 사용하여 KAS api를 요청한다.
  const eoaInfo = await caver.rpc.klay.getAccount(account);
  return eoaInfo;
}

// 3 . KAS basic krn에 신규 계정을 생성해주는 함수
async function makeAccount() {
//  caver-js를 사용하여 KAS api를 요청한다.
  const accountInfo = await caver.kas.wallet.createAccount();
  return accountInfo;
}

본격적으로 클레이튼 api를 쪼물딱 해봤다 할 수 있는 서버의 핵심 함수들이다.

구현이 간단한 편이기 때문에 시간이 오래 걸리지 않는다. makeAccount의 경우 KAS에서 제공하는 기본 저장소가 아닌 개인이 작성한 account pool에도 등록이 가능하다. 그렇게 하려면 요청 헤더를 본인이 발급 받은 krn으로 사용해야한다.

아래와 같이 express 를 사용해 요청을 처리하도록 했다.

app.get("/latestblock", (req, res) => {
  try {
    getLatestBlockNumber().then((result) => {
      res.status(200).json(result);
    });
  } catch (e) {
    console.log(e);
    res.status(400).send("Error");
  }
});

app.post("/eoainfo", (req, res) => {
  try {
    let account = req.body.account;
    if (!account) {
      res.status(400).send("Invalid Account");
    } else {
      getEoaInfo(account).then((result) => {
        console.log(result);
        res.status(200).json(result);
      });
    }
  } catch (e) {
    console.log(e);
    res.status(400).send("Error");
  }
});
app.get("/createaccount", (req, res) => {
  try {
    makeAccount().then((result) => {
      console.log(result);
      res.status(200).json(result);
    });
  } catch (e) {
    console.log(e);
    return e;
  }
});

Client Side

/my-kas-basic-app/src/App.js

// import "./App.css";
import axios from "axios";
import React, { useEffect, useState } from "react";

function App() {
  const [latestBlock, setLatestBlock] = useState("");
  const [eoaInfo, setEoaInfo] = useState({});
  const [newAccount, setNewAccount] = useState({});

  //삽질기록, 객체 사용 지양
  // let eoa_info = {
  //   address: eoaInfo,
  //   balance: "0",
  // };

  const handleEOAinfo = (e) => {
    setEoaInfo(e.target.value);
  };

  async function getLatestBlock() {
    let result = await axios.get("http://localhost:8080/latestblock");
    setLatestBlock(result.data);
  }
  async function getEoaInfo() {
    let result = await axios.post("http://localhost:8080/eoainfo", {
      account: eoaInfo,
    });
    // console.log(result.data.account.balance);
    // eoa_info.balance = result.data.account.balance.toString();
    // parseInt(hexString, 16);
    setEoaInfo({
      address: eoaInfo,
      balance: parseInt(result.data.account.balance, 16), //HEX to DEC
    });
  }

  async function getNewAccount() {
    let result = await axios.get("http://localhost:8080/createaccount");
    // console.log(result.data);
    setNewAccount({
      address: result.data.address,
      chainId: result.data.chainId,
      krn: result.data.krn,
      publicKey: result.data.publicKey,
      updatedAt: result.data.updatedAt,
    });
  }
  useEffect(() => {
    console.log("update");
  }, [latestBlock, eoaInfo]);
  return (
    <div className="App">
      <header className="App-header">
        <h1>KAS Basic test using caver-js</h1>
        <h1>All test is possible in BAOBAB Testnet</h1>
        <h3>You can request Lastest Block Numnber here</h3>
        <div>Latest Block address</div>
        <button onClick={getLatestBlock}>Latest Block</button>
        <div>Lastest Block : {latestBlock}</div>
        <br />

        <h3>...And You can check your EOA Information Here</h3>
        <div>Enter EOA address</div>
        <input onChange={handleEOAinfo}></input>
        <button onClick={getEoaInfo}>EOA Info</button>
        <div>address : {eoaInfo.address}</div>
        <div>Lastest Block : {eoaInfo.balance}</div>

        <h3>
          ...Or You can make new account here, and this account will be saved in
          KAS basic KRN
        </h3>
        <button onClick={getNewAccount}>Give Me New Account</button>
        <div>address : {newAccount.address}</div>
        <div>chainId : {newAccount.chainId}</div>
        <div>krn : {newAccount.krn}</div>
        <div>publicKey : {newAccount.publicKey}</div>
        <div>updatedAt : {newAccount.updatedAt}</div>
      </header>
    </div>
  );
}

export default App;

삽질 기록을.. 또 남겨두었다. 


1. 최신 블록 번호 요청

async function getLatestBlock() {
    let result = await axios.get("http://localhost:8080/latestblock");
    setLatestBlock(result.data);
  }
  
  ...
  
  
  return(
  	...
    <button onClick={getLatestBlock}>Latest Block</button>
    <div>Lastest Block : {latestBlock}</div>
    ...
  )

axios로 데이터를 요청하는 경우 응답 바디에 접근하려면 result.data로 접근해야한다.

위와 같은 요청의 경우 응답은 다음과 같다

출처 : https://docs.klaytnapi.com/tutorial/klaytn-node-api


2. 외부 계정 정보 검색

// handleEOAinfo 함수를 사용하여 input element에 입력된 텍스트를 eoaInfo state로 업데이트
const handleEOAinfo = (e) => {
    setEoaInfo(e.target.value);
  };
  
  ...
  
  async function getEoaInfo() {
    // post 요청을 사용하여 Eoa정보 요청
    let result = await axios.post("http://localhost:8080/eoainfo", {
      account: eoaInfo,
    });
    // console.log(result.data.account.balance);
    // eoa_info.balance = result.data.account.balance.toString();
    // parseInt(hexString, 16);
    
    // address와 계정 잔고로 구성하여 데이터 렌더링
    setEoaInfo({
      address: eoaInfo,
      balance: parseInt(result.data.account.balance, 16), //HEX to DEC
    });
  }
  
  ...
  return(
  		<input onChange={handleEOAinfo}></input>
        <button onClick={getEoaInfo}>EOA Info</button>
        <div>address : {eoaInfo.address}</div>
        <div>Lastest Block : {eoaInfo.balance}</div>
  )

출처 :  출처 : https://docs.klaytnapi.com/tutorial/klaytn-node-api

계정 정보를 요청하면 위와 같은 응답을 받게 되는데, 그 중 잔고의 경우 16진수로 데이터를 송신하기 때문에, 보기 편하게 10진수로 변경해주었다. nonce값 등을 포함해서 클라이언트에 전달하게 만들 수도 있지만, 일단은 간단한 형태로 구현하기 위해서 address와 balance로만 구성해서 데이터를 렌더링했다.

(물론, klay단위로 변경하면 더 좋을것이다..)


3. 새 계정 생성

async function getNewAccount() {
    let result = await axios.get("http://localhost:8080/createaccount");
    // console.log(result.data);
    setNewAccount({
      address: result.data.address,
      chainId: result.data.chainId,
      krn: result.data.krn,
      publicKey: result.data.publicKey,
      updatedAt: result.data.updatedAt,
    });
  }
  
  ...
  
  return(
   <button onClick={getNewAccount}>Give Me New Account</button>
        <div>address : {newAccount.address}</div>
        <div>chainId : {newAccount.chainId}</div>
        <div>krn : {newAccount.krn}</div>
        <div>publicKey : {newAccount.publicKey}</div>
        <div>updatedAt : {newAccount.updatedAt}</div>
        )

출처 : https://docs.klaytnapi.com/tutorial/wallet-api/wallet-account-api

계정 생성을 할 때에는 객체 형식의 state로 정보를 업데이트했다.

 


동작 확인

모든 기능이 클라이언트 사이드에서도 정상적으로 동작함을 확인했다.


마치며..

아쉬운 점

이 프로젝트를 작성하기에 앞서 web3로 프로젝트를 동기분과 함께 작성했는데, 서버 사이드에서 기능별로 파일을 나눠서 작성하는 습관을 들이는것이 좋을 것 같다는 충고를 들은 적이 있다. 나는 모듈 단위의 코드 작성에 익숙하지 않은 코린이..라서 자꾸만 단일 파일에 모든 코드를 작성하려고 하는데, 가독성이 매우 떨어지고 개발한 사람 이외에는 알아보기 힘들것같다. 이런 습관을 꼭 개선해야겠다고 느꼈다.

간단한 API 요청만 다루려고 하다 보니 klaytn API의 더 많은 기능들을 다뤄보지 못했는데, 서비스를 기획하다보면 KAS의 버전호환문제를 느끼게 된다고 한다. (...) 물론 사서 고생하고 싶은건 아니지만 그래도 좋은 개발자가 되기 위해 또 열심히 삽질을..해야겠다는 다짐이 생긴다.

 

좋았던

web3에 이어서 간단하게나마 caver까지 만져볼 수 있어서 좋았다. 그리고, 배운지 오래돼서 가물가물한.. react와 node js Express까지 다시 기억나게 해주는 좋은 프로젝트였다. 간단한 로컬 서버-클라이언트 프로젝트이기 때문에 많은 기능을 담지는 않았지만, 추후에 더 확장하고 다듬는다면 좋은 개인 프로젝트가 될 것 같다. 오랜만에 만져보는 리액트, express였기에 또 오랜 시간 삽질을..했다.

클라이언트 단에서는 리액트에 객체 랜더링 문제라던지, useEffect hook을 사용해 주지 않으면 랜더링이 바로 안되는 문제라던지, 예전에도 겪었던 문제들이라 금방금방 해결할 수 있었지만 그래도 생각보다 오래 삽질을 했다. 서버 단에서는 초반에 fetch를 사용하다가 꽤나 고생을 했다. (fetch는 죄없어..내잘못이야...) 난 앞으로 axios를 애용할 것 같다..

 

개선점

더 많은 기능을 구현하고 싶다. KAS에는 계정과 rpc 관련 기능 이외에도 컨트랙트, Token, NFT 관련한 다양한 API를 제공한다. 이 기능들을 다 사용해보기는 조금 어렵겠지만 특히 토큰 관련 API는 나중에 꼭 사용해볼것이다..

그리고 간단한 구현을 위해 css를 배제했는데,

styled component를 사용해서 간단하게나마 css를 입혀주면 좀 더 보기 좋을것같다.

web3의 경우 리액트 라우터를 사용해서 페이지 라우팅도 처리해줬는데, KAS는 하지 않았다. 이것도 신경 써서 코딩한다면 더 좋은 프로젝트가 될 것 같다.

 

 


이 프로젝트는 아래 깃허브에서 확인 가능합니다. 

Github - https://github.com/EunsuGoh/KASbasic/tree/main/my-kas-basic-app

 

GitHub - EunsuGoh/KASbasic

Contribute to EunsuGoh/KASbasic development by creating an account on GitHub.

github.com

 

'BlockChain' 카테고리의 다른 글

프로젝트2 회고 - Web2.0 blockchain community 만들기  (0) 2022.05.02
OpenSea Clone Coding Project - Whale  (0) 2022.04.18
Upgrade ERC20 - Owner 추가 및 Token Lock 재사용  (0) 2022.04.06
Eggman  (0) 2022.03.30
commit reveal scheme  (0) 2022.03.22