Incentive Community 프로젝트

윤장원·2022년 8월 29일
0

프로젝트

목록 보기
2/2
post-thumbnail

두 번째 프로젝트는 Web2.0에서 블록체인 인센티브를 기반으로, 커뮤니티 사이트를 개발하기이다. 사용자가 게시글을 작성하면 보상으로 토큰을 받고, 받은 토큰으로 NFT 거래 및 다른 사용자와 토큰 교환을 할 수 있다.

[github] https://github.com/codestates/BEB-05-Beginners

구현 목표

  • 회원 가입시 서버에서 사용자에게 지갑 주소를 부여하고, 사용자 정보와 지갑 주소를 데이터베이스에 저장한다.
  • Article로 이동하면 커뮤니티에 작성된 게시글들을 볼 수 있다.
  • 커뮤니티에 게시글을 작성하면 보상으로 ERC-20토큰을 10개 지급한다.
  • 해당 게시글을 작성한 사용자만 게시글을 수정 및 삭제를 할 수 있다.
  • Mypage로 이동하면 내 정보를 볼 수 있다.(지갑 주소, 보유 토큰 수, 보유 eth)
  • Mypage에서 eth Faucet 버튼을 누르면 서버에서 1 eth를 지급한다.
  • Mypage에서 다른 지갑으로 내가 가진 토큰을 전송할 수 있다.

구성 페이지

  • 처음 사이트에 접속했을 때 보이는 Main
  • 회원가입 할 수 있는 Signup
  • 로그인 할 수 있는 Login
  • 작성된 게시글들을 볼 수 있는 Article
  • 게시글을 작성할 수 있는 Post
  • NFT 목록을 볼 수 있는 NFT
  • 내 정보를 볼 수 있고, 토큰을 다른 지갑으로 전송할 수 있는 Mypage

역할 분담

프로젝트는 3명이서 진행했다. 나는 서버와 DB, 그리고 ERC-20 토큰을 다루는 스마트 컨트랙트를 맡았다.
DB는 MySQL을 이용하여 사용자 계정, 게시글, NFT를 관리했다.
서버에서는 회원가입, 로그인, 로그아웃, 사용자 정보 조회, 토큰 전송, ETH Faucet 받기, 게시글(작성, 조회, 수정, 삭제) 기능을 구현했다.

관계형 데이터베이스 구현

테이블 구성은 다음과 같다. NFT 정보를 담는 NFT 테이블, 게시물의 정보를 담는 Post 테이블, 그리고 유저의 정보를 담는 User 테이블이 있다.

schema.sql

CREATE TABLE User (
  user_id varchar(100),
  user_password varchar(100),
  user_address varchar(200),
  user_privateKey varchar(200),
  user_eth varchar(100),
  user_token varchar(100),
  created_at datetime DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY(user_id)
);

CREATE TABLE Post (
  id INT AUTO_INCREMENT,
  user_id varchar(100),
  post_title varchar(200),
  post_content varchar(100),
  created_at datetime DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY(id)
);

CREATE TABLE NFT (
  token_id INT,
  token_img varchar(200),
  token_name varchar(100),
  user_id varchar(100),
  contract_address varchar(100),
  token_description varchar(500),
  created_at datetime DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY(token_id)
);

ALTER TABLE Post ADD FOREIGN KEY (user_id) REFERENCES User (user_id);
ALTER TABLE NFT ADD FOREIGN KEY (user_id) REFERENCES User (user_id);

사용자 지갑 생성

사용자가 회원가입을 하게 되면, 서버에서 web3.eth.accounts.create()을 이용해 지갑을 생성한다. 그 다음 지갑의 주소와 개인키를 User Table에 저장했다.

signUpController.js

const db = require("../db");
const Web3 = require("web3");
const rpcURL = "https://ropsten.infura.io/v3/인퓨라endpoint";
const web3 = new Web3(rpcURL);

module.exports = {
  signUp: (req, res) => {
    const { user_id, user_password } = req.body;

    const findAccount = `SELECT * FROM User WHERE user_id = "${user_id}"`;
    db.query(findAccount, (error, result) => {
      if (result[0]) {
        res.status(409).json({ message: "Id exists" });
      } else {
        const createAddress = web3.eth.accounts.create(user_password);
        const user_address = createAddress.address;
        const user_privateKey = createAddress.privateKey;

        const insertAccount = `INSERT INTO User (user_id, user_password, user_address, user_privateKey, user_eth, user_token) VALUES ("${user_id}","${user_password}" , "${user_address}", "${user_privateKey}", "0", "0")`;

        db.query(insertAccount, (error, result) => {
          if (error) {
            return res.status(500).send("Internal Server Error");
          } else {
            res.status(201).json({ message: "sign up success!" });
          }
        });
      }
    });
  },
};

Eth Faucet 받기

먼저 body에 user_id를 담은 요청을 받게 되면, 데이터베이스에서 해당 user_id에 맞는 지갑 주소를 찾는다. 그 다음 web3.eth.accounts.signTransaction()을 써서 트랜잭션에 서명을 하고, web3.eth.sendSignedTransaction()을 써서 서명한 트랜잭션을 전송한다. 트랜잭션 전송이 끝난 후 web3.eth.getBalance()을 써서 지갑 주소의 잔액 정보를 가져와 User 테이블 user_eth 정보를 업데이트 한다.

ethFaucetController.js

const db = require("../db");
const Web3 = require("web3");
const web3 = new Web3("HTTP://127.0.0.1:7545");

module.exports = {
  ethFaucet: (req, res) => {
    const { user_id } = req.body;

    const findUserAddress = `SELECT user_address FROM User WHERE user_id = "${user_id}"`;
    db.query(findUserAddress, (error, result) => {
      if (error) {
        console.log(error);
      } else {
        const ganacheAddress = "서버 계정 주소";
        const ganachePrivateKey =
          "서버 계정 개인키";
        const user_address = result[0].user_address;

        web3.eth.accounts
          .signTransaction(
            {
              to: user_address,
              value: "1000000000000000000",
              gas: 2000000,
            },
            ganachePrivateKey
          )
          .then((value) => {
            return value.rawTransaction;
          })
          .then(async (tx) => {
            web3.eth.sendSignedTransaction(tx, async function (err, hash) {
              if (!err) {
                const userBalance = await web3.eth
                  .getBalance(user_address)
                  .then((balance) => {
                    return web3.utils.fromWei(`${balance}`);
                  });
                const updateUserEth = `UPDATE User SET user_eth="${userBalance}" WHERE user_address = "${user_address}"`;
                db.query(updateUserEth, (error, result) => {
                  if (error) {
                    console.log(error);
                  } else {
                    return res
                      .status(201)
                      .json({ message: "eth Faucet Success!" });
                  }
                });
              } else {
                console.log("Faucet error!");
              }
            });
          });
      }
    });
  },
};

ERC-20 토큰

1. 게시글 작성시 서버에서 사용자 지갑으로 토큰 지급

사용자가 Post 페이지에서 게시글을 작성 하면, 데이터베이스에서 해당 user_id에 맞는 지갑 주소를 찾는다. 그 다음 transfer 함수를 이용해 서버에서 사용자 지갑으로 10 토큰을 지급한다. 토큰 지급이 완료되면, balanceOf 함수를 이용해 사용자 지갑의 토큰 잔액 정보를 가져와 User 테이블 user_token 정보를 업데이트 한다.

articleController.js

const models = require("../models/article");
const db = require("../db");
const Web3 = require("web3");
const web3 = new Web3("HTTP://127.0.0.1:7545");
const erc20abi = require("../contract/erc20abi");

module.exports = {
  create: (req, res) => {
    const { user_id, post_title, post_content } = req.body;

    models.create(user_id, post_title, post_content, (error, result) => {
      if (error) {
        console.log(error);
        return res.status(500).send("Internal Server Error");
      } else {
        const queryString = `SELECT user_address FROM User WHERE user_id = "${user_id}"`;
        db.query(queryString, (error, result) => {
          if (error) {
            console.log(error);
          } else {
            const contractAddr = "컨트랙트 주소";
            const serverAddress = "서버 계정 주소";
            const serverPrivateKey =
              "서버 계정 개인키";
            const userAddress = result[0].user_address;
            const contract = new web3.eth.Contract(erc20abi, contractAddr, {
              from: serverAddress,
            });
            const data = contract.methods.transfer(userAddress, 10).encodeABI();

            const rawTransaction = {
              to: contractAddr,
              gas: 100000,
              data: data,
            };

            async function getTOKENBalanceOf(address) {
              const balance = await contract.methods.balanceOf(address).call();
              return balance;
            }

            web3.eth.accounts
              .signTransaction(rawTransaction, serverPrivateKey)
              .then((signedTx) =>
                web3.eth.sendSignedTransaction(signedTx.rawTransaction)
              )
              .then((req) => {
                getTOKENBalanceOf(userAddress).then((balance) => {
                  const updateBalance = `UPDATE User SET user_token="${balance}" WHERE user_address = "${userAddress}"`;
                  db.query(updateBalance, (error, result) => {
                    if (error) {
                      console.log(error);
                    } else {
                      return res.status(201).json({ message: "success!" });
                    }
                  });
                });
                return true;
              });
          }
        });
      }
    });
  },
}

2. 사용자가 다른 지갑 주소로 토큰 전송

사용자가 Mypage 페이지에서 토큰 전송할 지갑 주소와 토큰의 양을 입력하고 transfer 버튼을 누른다. 데이터베이스에서 해당 user_id에 맞는 지갑 주소와 개인키를 가져오고, transfer 함수를 이용해 사용자 지갑에서 입력한 지갑 주소로 토큰을 전송한다. 토큰 전송이 완료되면 balanceOf 함수를 이용해 사용자 지갑의 토큰 잔액 정보와 입력한 지갑 주소의 토큰 잔액 정보를 가져와 User 테이블 user_token 정보를 업데이트 한다.

transferController.js

const db = require("../db");
const Web3 = require("web3");
const web3 = new Web3("HTTP://127.0.0.1:7545");
const erc20abi = require("../contract/erc20abi");

module.exports = {
  transfer: (req, res) => {
    const { user_id, toAddress, tokenAmount } = req.body;

    const findUserAddressPassword = `SELECT user_address, user_privateKey FROM User WHERE user_id = "${user_id}"`;
    db.query(findUserAddressPassword, (error, result) => {
      if (error) {
        console.log(error);
      } else {
        const user_address = result[0].user_address;
        const user_privateKey = result[0].user_privateKey;
        const contractAddr = "컨트랙트 주소";

        const contract = new web3.eth.Contract(erc20abi, contractAddr, {
          from: user_address,
        });
        const data = contract.methods
          .transfer(toAddress, tokenAmount)
          .encodeABI();

        const rawTransaction = {
          to: contractAddr,
          gas: 100000,
          data: data,
        };

        async function getTOKENBalanceOf(address) {
          const balance = await contract.methods.balanceOf(address).call();
          return balance;
        }

        web3.eth.accounts
          .signTransaction(rawTransaction, user_privateKey)
          .then((signedTx) =>
            web3.eth.sendSignedTransaction(signedTx.rawTransaction)
          )
          .then((req) => {
            getTOKENBalanceOf(user_address).then((balance) => {
              const updateBalance = `UPDATE User SET user_token="${balance}" WHERE user_address = "${user_address}"`;
              db.query(updateBalance, (error, result) => {
                if (error) {
                  console.log(error);
                } else {
                  getTOKENBalanceOf(toAddress).then((balance) => {
                    const updateToBalance = `UPDATE User SET user_token="${balance}" WHERE user_address = "${toAddress}"`;
                    db.query(updateToBalance, (error, result) => {
                      if (error) {
                        console.log(error);
                      } else {
                        return res.status(201).json({ message: "success!" });
                      }
                    });
                  });
                }
              });
            });
            return true;
          });
      }
    });
  },
};

서버 API 구현

[API 명세서] https://github.com/codestates/BEB-05-Beginners/wiki

1. 회원가입

Request
POST /signup

Body

ParameterTypeDescriptionNecessary
user_id문자열유저의 아이디필수
user_password문자열유저의 비밀번호필수

Response

응답은 다음과 같다.

1) 해당 아이디가 이미 존재할 경우
409 상태코드와 "ID exists" 메세지로 응답한다.

2) 회원가입에 성공할 경우
201 상태코드와 "sign up success!"메세지로 응답하고 데이터베이스에 유저 정보를 저장한다.

ex) 'yjjjwww'로 회원가입 했을 때 User 테이블

2. 로그인

Request
POST /login

Body

ParameterTypeDescriptionNecessary
user_id문자열유저의 아이디필수
user_password문자열유저의 비밀번호필수

Response

응답은 다음과 같다.

1) 데이터베이스에 저장된 유저의 정보와 다를 경우
403 상태코드와 "not authorized" 메세지로 응답한다.

2) 회원가입에 성공할 경우
201 상태코드와 "Login success!"메세지로 응답한다.
session에 user_id 값이 추가된다.

3. 로그아웃

Request
POST /logout

Response
session에 로그인 기록이 있을 경우 로그아웃 된다.

4. 사용자 정보 조회

Request
GET /userinfo

Response

session에 로그인 기록이 있을 경우 해당 user_id 데이터를 가져온다.

응답은 다음과 같은 json형태이다.

{
    "data": [
        {
            "user_id": "codestates",
            "user_password": "1234",
            "user_address": "0x4a648c79Fad1FCa52CC66C37811c805a546284a0",
            "user_privateKey": "0x36d77dcab8d875a710cbae43158494d9fb0ac2e04fe9d7f3e1aadf653e6b3faa",
            "user_eth": "2.99675424",
            "user_token": "82",
            "created_at": "2022-08-27T21:41:34.000Z"
        }
    ],
    "message": "ok"
}

5. 게시글 작성

Request
POST /article

Body

ParameterTypeDescriptionNecessary
user_id문자열유저의 아이디필수
post_title문자열게시글 제목필수
post_content문자열게시글 내용필수

Response

응답은 다음과 같다.

1) 201 상태코드와 "success!" 메세지로 응답하고 게시글 정보를 데이터베이스에 저장합니다.
2) 게시글을 작성한 유저에게 보상으로 10토큰을 지급합니다.

ex) 'yjjjwww' 아이디로 게시글 작성했을 때 Post 테이블

ex) 게시글 작성 후 'yjjjwww' 아이디로 10토큰 지급

6. 모든 게시글 조회

Request
GET /article

Response
응답은 다음과 같은 json 데이터를 담고있는 배열이다.

[
    {
        "id": 3,
        "user_id": "test",
        "post_title": "테스트 게시글",
        "post_content": "테스트입니다.",
        "created_at": "2022-08-21T13:14:11.000Z"
    },
]

7. 게시글 수정

Request
PUT /article

Body

ParameterTypeDescriptionNecessary
user_id문자열유저의 아이디필수
post_title문자열게시글 제목필수
post_content문자열수정할 게시글 내용필수

Response
요청 받은 유저의 아이디와 게시글 제목에 해당하는 게시글의 내용을 받은 post_content로 수정하고 데이터베이스에 저장한다.

ex) 'yjjjwww' 아이디의 게시글을 수정했을 때 Post 테이블

8. 게시글 삭제

Request
DELETE /article

Body

ParameterTypeDescriptionNecessary
user_id문자열유저의 아이디필수
post_title문자열게시글 제목필수

Response
요청 받은 유저의 아이디와 게시글 제목에 해당하는 게시글을 데이터베이스에서 삭제한다.

ex) 'yjjjwww' 아이디의 게시글을 삭제했을 때 Post 테이블

9. ETH Faucet 받기

Request
POST /faucet

Body

ParameterTypeDescriptionNecessary
user_id문자열유저의 아이디필수

Response
서버 계정에서 사용자 지갑으로 1eth를 보내고, "eth Faucet Success!" 메세지로 응답한다.

ex) 'yjjjwww' 아이디로 ETH Faucet 받은 후 User 테이블

10. 토큰 전송하기

Request
POST /transfer

Body

ParameterTypeDescriptionNecessary
user_id문자열유저의 아이디필수
toAddress문자열토큰 전송할 지갑 주소필수
tokenAmount문자열전송할 토큰 양필수

Response
토큰 전송을 완료한 후 201 상태코드와 "success!"메세지로 응답한다.

ex) 'yjjjwww' 아이디에서 'codestates' 아이디로 3토큰 전송한 후 User 테이블

프론트엔드

Main 페이지

Signup 페이지

Login 페이지

Article 페이지 - 커뮤니티에 작성된 모든 게시들을 볼 수 있다.

Post 페이지 - 게시글 작성하는 페이지

NFT 페이지

Mypage 페이지 - 나의 정보를 볼 수 있고, ETH Faucet 받기, 다른 지갑으로 토큰 전송을 할 수 있다.

프로젝트 회고

두 번째 프로젝트에서도 서버와 DB 부분을 작업했고, 이번에는 추가로 ERC-20토큰 기능을 다루는 스마트 컨트랙트 부분까지 작업했다. 초기 서버 환경 세팅이나 API 설정 및 기능 구현은 이제 좀 익숙해진 거 같았지만, 스마트 컨트랙트 부분을 작업할 때 web3.js를 다루는 과정에서 에러가 자주 뜨는 어려움이 있었다. 그리고 내 로컬환경의 가나슈 계정으로 컨트랙트를 배포해서 내 컴퓨터로 서버 기능 구현은 정상적으로 됐지만, 팀원들의 컴퓨터에서는 토큰 전송이 정상적으로 되지 않는 문제가 발생했다. 그 해결책으로 ngrok을 이용하여 로컬 개발 환경을 공유하는 방법을 써 봤는데 CORS 오류를 해결하지 못했다. 해당 문제는 공부를 더 해서 해결해 볼 생각이다.

이번에 팀장의 역할을 맡아 프로젝트를 진행했는데, 프로젝트 기획이나 시간에 따른 프로젝트 진행 관리를 잘 하지 못해서 구현해야하는 기초적인 기능들만 구현했고 추가로 우리가 원하는 기능을 더 넣지 못해 아쉬웠다. 부트캠프가 끝난 뒤 이번 프로젝트에 기능을 더 추가해서 여러가지 활동을 할 수 있는 커뮤니티 사이트를 만들어 볼 계획이다.

0개의 댓글