[Savvy]
http://savvy-front.s3-website.ap-northeast-2.amazonaws.com/
[GitHuB Repository]
https://github.com/codestates-beb/beb-09-final-Savvy
GitHub - codestates-beb/beb-09-final-Savvy
Contribute to codestates-beb/beb-09-final-Savvy development by creating an account on GitHub.
github.com
Savvy
코드스테이츠 부트캠프에서의 마지막 프로젝트를 끝마쳤다. 마지막 프로젝트는 자유주제였으며 4주간 진행되었다.
이전의 프로젝트를 함께 진행했던 팀장님이 마지막 프로젝트 기간에 접어들기 전 아이디어를 제시하며 함께 해보는 것이 어떻냐고 제안하였다. 그 내용은 새로운 기술인 ERC-6551을 사용하여 프로젝트를 진행해 보자는 것이었고 블록체인 관련하여 새로운 기술을 이전부터 접해보고 싶던 나에게는 거절할 이유가 없었기에 흔쾌히 참여하였다.
먼저 ERC-6551이 무엇인가에 대하여 간단하게 설명해 보겠다. ERC-6551은 NFT와 SmartContract를 추상화시킨 일종의 NFT Wallet, 즉 TokenBoundAccount(TBA)를 만드는 표준이다. ERC-6551이 있기 이전 까지는 개별 NFT마다 주소가 없어서 해당 NFT를 가지고 있는 홀더 자체를 트래킹 해야 했고 홀더가 NFT를 제삼자에게 팔거나 전송할 시 기존 NFT 홀더에 대한 정보는 모두 사라지게 됐다. ERC-6551(TBA)를 이용한다면 개별 NFT에도 주소가 부여되고 해당 NFT Wallet은 다양한 아이템을 가지고 있을 수 있어 홀더에게 귀속되지 않고 특정 NFT가 커뮤니티에 기여한 이력을 연속성 있게 확인할 수 있게 된다.
기존 NFT 커뮤니티를 관리할때, NFT홀더들이 스냅숏을 찍어 특정한 토큰을 에어드롭해줘야 하는 경우가 존재하며 이에 필요한 커뮤니티 참여자들에 대한 아이덴티티 파악이 힘든 경우가 대부분이다. Savvy는 이러한 문제를 ERC-6551을 통해 해결한 ERC-6551 기반 커뮤니티 관리 툴이다. Savvy가 제공하는 주요한 서비스는 다음과 같다.
- 커뮤니티 참여자들에 대한 실시간 모니터링
- 원클릭으로 스냅샷부터 에어드롭까지
- 이벤트 티켓 NFT를 쉽게 배포
- 하나의 계정으로 여러 개의 커뮤니티를 관리
확실히 아무래도 새로운 기술을 가지고 프로젝트를 진행하다 보니 부딪히는 문제점이 한두 개가 아니었다. 문서들을 찾아 적용하는 것도 쉽지 않았으며 일단은 ERC-6551이 어떤 것인지 이해하는 과정도 쉽지 않았다. 팀원들과 아래와 같이 다양한 문서를 각자 찾아 조사하고 조사한 내용들을 공유하고 정리하며 이해했다.
이번 프로젝트에서도 새로운 경험들을 많이 했다. 새로운 DB(MongoDB)를 사용하고 AWS 서비스를 통해 배포하는 과정을 경험해 보았으며 블록체인 네트워크에서의 실시간 Tracking을 구현하는 과정에서 많은 새로운 사실들을 알게 되었다. 새로운 기술을 가지고 프로젝트를 진행하다 보니 단순히 문서를 찾고 이해하는 시간부터 오래 걸렸지만 이러한 과정 또한 앞으로 새로운 기술들을 받아들이고 터득해 나가는 데 있어서 많은 도움이 될 것이라 생각한다. 쉽지 않은 여정이었지만 함께 애써준 팀장님을 포함한 팀원들 덕분에 프로젝트를 잘 마무리하였고 많은 것을 얻어갈 수 있었다.
역할
코드스테이츠에서의 마지막 프로젝트도 4명에서 팀을 이뤄 진행했다. 팀원은 이전 프로젝트를 함께했던 팀장 박 xx(총괄)님, 한 xx(FrontEnd), 첫 번째 프로젝트를 함께했던 이 xx(BackEnd)님, 그리고 나(BackEnd)로 구성되었다. 확실히 이전에 함께 프로젝트를 했던 팀원들이라 그런지 소통과 프로젝트 진행에 있어서 큰 어려움이 없었으며 각자가 경험해보고 싶은 부분들을 얘기하고 분배하여 팀원들 전부가 프로젝트를 통해 얻어가는 부분도 많았다.
목표
핵심 기능
- NFT 홀더에게 토큰(NFT&ERC20) & 커뮤니티 이벤트 티켓(NFT) 에어드랍 하는 기능
- NFT 자체에 대한 정보 나타내기
- 쿠폰처럼 NFT를 에어드롭하는 기능
- 구독 서비스 구현
부수 기능
- 유저 서비스
- ERC-6551 TBA를 만들 수 있는 서비스
- 메타마스크 연동하기
- 관리자 서비스
- 회원가입-지갑계정과 연동(web3 auth 사용)
- 로그인 / 로그아웃
- 커뮤니티 관련된 NFT 컬렉션이나 ERC20 토큰 컨트랙트를 등록하여 관리하는 기능
프로젝트 기획
DB Schema
http://52.79.163.209:8080/api-docs/
API는 아래와 같이 API리스트를 문서화하여 팀원들과 소통했으며 Swagger를 사용하여 API DOCS를 관리했다
WireFrame
FlowChart
Service Architecture
Stack
- Frontend: React, MUI(Material UI) Library
- Backend: NodeJS, Express.js, Pinata/SDK, ethers.js, MongoDB, swagger
- Smart Contract: Solidity, Foundry
- Deployment : AWS EC2, AWS S3
작업 내용
우리 팀은 아래와 같이 Notion페이지를 통해 프로젝트 내에서 각자가 맡은 부분을 보며 새롭게 알게 되어 공부한 내용들을 공유하고 부딪히는 부분들은 함께 해결했다.
아래는 이번 프로젝트에서 내가 맡은 특징적인 부분들을 정리한 내용이다. 여기에 더해서 초기환경 세팅, API 구현, 관리 등과 같은 기본적인 BackEnd작업들을 맡아하였다.
- node.js express framework를 사용하여 server 구현
- Mongoose(ODM)를 사용하여 MongoDB 연동
- 실시간 Tracking Daemon.js 구현
- AWS EC2 서비스를 사용하여 Server 배포
- AWS S3 서비스를 사용하여 Client 배포
먼저 Server폴더의 구조는 위와 같이 구성하였다.
DB는 지난번과 달리 MySQL이 아니라 MongoDB를 사용하였다. NoSQL은 처음 써보는 DB의 형태라 MySQL과 많이 달랐지만 사용하는데 어려움은 크게 없었고 오히려 사용하는 과정이 더 편리하게 느껴졌다.
app.js (MongoDB 연결)
const mongoose = require("mongoose");
// const userInfo = require('./config/userinfo.json');
mongoose
.connect(process.env.MONGODB_URL, {
useNewUrlParser: true,
useUnifiedTopology: true,
user: process.env.MONGODB_USERNAME,
pass: process.env.MONGODB_PASSWORD,
})
.then(() => console.log("MongoDB에 연결되었습니다."))
.catch((err) => console.error("MongoDB에 연결할 수 없습니다.", err));
아래는 실시간 Tracking을 위한 Daemon.js 파일의 코드이다.
require("dotenv").config();
const { ethers } = require("ethers");
const mongoose = require("mongoose");
const Tba = require('./models/tba.model');
const Community = require('./models/community.model');
// ERC6551 레지스트리 컨트랙트의 ABI (함수 인터페이스)
const erc6551RegistryABI = require('./abi/ERC6551Registry.json');
const accountCreatedABI = require('./abi/Account.json');
const nftContractAbi = require('./abi/NftContract.json');
// ERC6551 레지스트리 컨트랙트 주소
const erc6551RegistryAddress = "0x02101dfB77FDE026414827Fdc604ddAF224F0921";
// Ethereum JSON-RPC 프로바이더 생성
const providerUrl = process.env.INFURA_URL;
const provider = new ethers.providers.JsonRpcProvider(providerUrl);
// MongoDB 연결
mongoose
.connect(process.env.MONGODB_URL, {
useNewUrlParser: true,
useUnifiedTopology: true,
user: process.env.MONGODB_USERNAME,
pass: process.env.MONGODB_PASSWORD,
})
.then(() => console.log('MongoDB에 연결되었습니다.'))
.catch((err) => console.error('MongoDB에 연결할 수 없습니다.', err));
// Ethereum 컨트랙트 및 이벤트 구독
const erc6551RegistryContract = new ethers.Contract(
erc6551RegistryAddress,
erc6551RegistryABI,
provider
);
const eventFilter = erc6551RegistryContract.filters.AccountCreated();
// 이벤트 리스너에서 감지된 트랜잭션을 처리하는 부분
erc6551RegistryContract.on(eventFilter, async (account, implementation, chainId, tokenContract, tokenId, salt, event) => {
console.log("tba 생성 감지");
// 토큰 컨트랙트가 Community 컬렉션에 있는 커뮤니티 주소들과 일치하는지 확인하기 위해 모든 커뮤니티 주소를 가져옴
const communityAddresses = await Community.find({}, { address: 1 });
// tokenContract가 communityAddresses 배열에 포함된 주소들과 일치하는지 확인하여 필터링
const isMatchingContract = communityAddresses.some(community => community.address === tokenContract);
// 만약 일치하는 커뮤니티 주소가 있을 때만 계정 정보 추가를 수행
if (isMatchingContract) {
console.log("ERC6551 레지스트리에서 새 트랜잭션 생성:", event);
// community_id 가져오기
try {
const community = await Community.findOne({ address: tokenContract });
const communityId = community ? community.id : null;
const accountContract = new ethers.Contract(
account,
accountCreatedABI,
provider
);
let owner = null;
try {
owner = await accountContract.owner();
} catch (error) {
console.log('Error occurred while getting owner');
}
// owner가 존재할 때만 계정 정보 추가
if (owner) {
const nftContract = new ethers.Contract(
tokenContract,
nftContractAbi,
provider
);
let tokenURI = null;
try {
tokenURI = await nftContract.tokenURI(tokenId);
} catch (error) {
console.log('Error occurred while getting tokenURI');
}
console.log(`token uri: ${tokenURI}`);
console.log(`tokenId:${tokenId}`);
// ethBalance 가져오기 (account의 이더 잔액 조회)
const balance = await provider.getBalance(account);
const ethBalance = ethers.utils.formatEther(balance);
// MongoDB에 계정 정보 추가
await Tba.create({
address: account,
owner: owner,
level: '',
tokenURI: tokenURI,
ethBalance: ethBalance,
createdAt: new Date(),
updatedAt: new Date(),
community_id: communityId,
});
console.log("계정 업데이트 완료");
}
} catch (error) {
console.error("DB 업데이트 오류:", error);
}
console.log("트랜잭션 처리 완료");
}
});
위 코드는 네트워크에서 ERC-6551, 즉 TBA가 생성되는 이벤트(AccountCreated)를 구독하여 생성된 TBA가 우리가 관리하는 Savvy에 속한 커뮤니티 주소에 해당하는 주소를 가지고 있는지 판단하여 DB에 데이터를 저장하는 코드이다.
아래는 Swagger를 통해 API를 관리하기 위해 작성한 코드이다. Router에서 Swagger 코드를 사용하였다. 처음에는 형식이 복잡해 보이고 귀찮았으나 금방 적응하고 여러 API에 적용해 나갔고 작업하고 나니 API Test를 간편하게 할 수 있고 공동 작업에 있어서 편리한 이점이 있어 좋았다.
const express = require('express');
const router = express.Router();
const controller = require('../controllers/admin.controller');
const { schema } = require('../models/community.model');
router.post('/login', controller.login, async (req, res) => {
// #swagger.description = 'login'
// #swagger.tags = ['Admin']
/* #swagger.security = [{
"bearerAuth": []
}] */
/* #swagger.requestBody = {
content: {
'application/json': {
schema: {
type: 'object',
properties: {
address: {
type: 'string',
example: '0xf05Fc7E8b21bE1133c0Ccae63eb6ebCdDF4e5F78'
},
balance: {
type: 'string',
example: '0x00'
},
chainId: {
type: 'string',
example: '11155111'
},
email: {
type: 'string',
example: 'sjlee80@gmail.com'
},
name: {
type: 'string',
example: '이상준'
},
profileImage: {
type: 'string',
example: 'https://lh3.googleusercontent.com/a/AAcHTteUzkPkbNWjxw4M6EzVs72GHuq7LWGFQyJL6iWT0gH5=s96-c'
},
appPubKey: {
type: 'string',
example: '03757c7b4796e8f42b02796607b5ee2ad23c6beb9a28e951777ac4349915b95aa2'
},
}
}
}
}
} */
/* #swagger.responses[200] = {
description: 'Verification successful and user exists',
schema: {
message: 'Verification successful. Welcome, new user!'
}
} */
/* #swagger.responses[201] = {
description: 'Verification successful. Welcome, new user',
schema: {
message: 'Verification successful. Welcome, new user!'
}
} */
/* #swagger.responses[400] = {
description: 'Verification Failed',
schema: {
error: 'Verification Failed'
}
} */
/* #swagger.responses[500] = {
description: 'Internal Server Error',
schema: {
error: 'Internal Server Error'
}
} */
});
router.get('/community', controller.getCommunity, async (req, res) => {
// #swagger.description = 'get community'
// #swagger.tags = ['Admin']
/* #swagger.security = [{
"bearerAuth": []
}] */
/* #swagger.responses[200] = {
description: 'get community data',
schema: {
message: 'get community data',
community: {
type: 'object',
properties: {
_id: "64cb5b86d25c4e600dccea92",
address: "0x7c01Eaa85063Aa3a310E49D6b80aC02a1532d16F",
type: "main",
alias: "test 1",
admin_id: "64cb5a7bd25c4e600dccea5c",
createdAt: "2023-08-03T07:47:18.056Z",
_somethingElse: 0,
}
}
}
} */
/* #swagger.responses[404] = {
description: 'Community does not exist',
schema: {
error: 'Community does not exist'
}
} */
/* #swagger.responses[500] = {
description: 'Internal Server Error',
schema: {
error: 'Internal Server Error'
}
} */
});
module.exports = router;
이번 프로젝트에서 배포를 도맡아 하였는데 AWS를 사용해 배포하는 과정은 처음 경험해 보는 것이었다.
클라이언트는 AWS S3를 통하여, Server는 AWS EC2를 통하여 배포하였다.
아래와 같이 이번에 작업하면서 공부한 AWS 내용들을 정리해 문서화해 두었다.
결과물
- 시작 페이지
- Setting 페이지
- Manager 페이지
- Airdrop 페이지
- Ticket 페이지
- Contract 페이지
- TBA 페이지
- Dashboard 페이지
마무리
CodeStates의 마지막 프로젝트는 자율 프로젝트였다. 그러한 만큼 팀원들과 함께 심혈을 기울여 작업했고 많이 가까워질 수 있었던 것 같다. 좋은 팀원들 덕분에 힘든 시간 속에서도 프로젝트를 잘 마무리했던 것 같아서 팀원들에게 우선 너무 감사했다고 얘기하고 싶다.
아무래도 새로운 기술인 ERC-6551을 가지고 프로젝트를 진행하다 보니 문서를 찾고 코드에 적용하는 것이 쉬운 일은 아니었지만 팀원들과의 잦은 소통이 ERC-6551을 이해하고 적용하는데 도움이 많이 되었고 덕분에 프로젝트를 잘 마무리할 수 있었다.
이번에 다양한 문서를 찾고 공부해 보면서 아직 백엔드, 블록체인에 대해 부족한 점이 정말 많구나를 깨달았고 앞으로의 경험으로써 이를 채워나갈 생각을 하니 설레는 마음도 생겼다. 프로젝트들을 진행하면서 항상 깨닫는 바가 있다. 시작 전엔 항상 걱정들로 가득 차있고 망설여지지만 일단 부딪히면 무엇이든 해결해 나갈 수 있고 보완해 나갈 수 있다는 것이다. 그렇게 함으로써 성장해 나갈 수 있다고 깨닫게 되었고 이로 인해 앞으로 맞닥뜨리는 어려움들을 설레는 마음으로 이겨나갈 수 있는 자신감이 생겼다.
CodeStates의 6개월이란 기간 동안 멤버들에게 나도 모르게 정이 많이 들었다. 그동안 다들 너무 고생했다고, 감사했다고 전하고 싶고 각자의 자리에서 노력하여 원하는 모습으로 만날 수 있길 고대한다.
'Project' 카테고리의 다른 글
[Review] 인센티브 커뮤니티 (0) | 2023.07.14 |
---|---|
[1주차] 인센티브 커뮤니티 (0) | 2023.07.10 |
[Review] 오픈씨 클론 코딩 (0) | 2023.06.30 |