Token 기반 커뮤니티 만들기
2주간 토이 프로젝트를 진행하였다. 목표는 ERC20 토큰 기반의 커뮤니티 사이트를 만드는 것이었다.
이를테면 코박 커뮤니티와 비슷한 개념이다.
개발 환경
- Ganache (블록체인)
- web3.js (블록체인, 서버)
- node.js (서버, 클라이언트)
- express.js (서버, 클라이언트)
- mysql (서버)
- react (클라이언트)
소스
- server (REST)
- daemon (서버 측 데몬)
- client
내가 만들고자 한 커뮤니티는 다음과 같은 기능을 가진다.
- 이용자가 로그인, 새 글, 댓글 작성 등 커뮤니티 활동을 하면 그에 따른 보상을 토큰(ERC20) 으로 지급한다. (토큰 지급)
- 이용자는 토큰을 다른 이용자와 교환할 수 있다. (토큰 전송)
- 이용자는 토큰으로 NFT (ERC721) 을 생성할 수 있다.
- 이용자는 토큰으로 NFT 를 판매/구매할 수 있다.
- 이용자는 토큰 및 NFT 을 교환 내역을 쉽게 확인할 수 있다.
좀 더 엔지니어링? 관점에서 관리자의 기능 측면을 보면 다음과 같다.
- 관리자는 이용자에게 새로운 지갑(이더리움) 을 발급한다.
- 관리자는 테스트용 ETH 를 발급(가스비를 위해) 할 수 있다.
- 관리자는 계약(ERC20 및 ERC721) 디플로이 기능을 자동화한다.
- 관리자는 이용자의 토큰 및 NFT 를 직접 관리한다.
- 관리자는 블록체인으로부터 토큰 및 NFT 데이터를 쉽게 제공한다
- 관리자는 이용자에게 토큰을 지급할 수 있다.
- 관리자는 NFT 생성을 위해 이용자 allowance 를 받아두었다가 지급 시 결제한다.
- 관리자는 이용자의 트랜잭션을 모아 두었다가 한번에 처리할 수 있다 (batch)
- 관리자는 커뮤니티 관련 트랜잭션을 별도의 DB 에 따로 보관할 수 있다
등등..
적다보니 무수히 많은 기능이 필요할 것 같다. 뭔가 뜬구름 잡는 얘기같으니 우선 클라이언트 이미지를 보면서 고민해보자.
참고로 모든 개발은 로컬 Ganache 환경에서 진행했다
블로그에서 보이는 모든 소스는 Github 를 참조하길 바란다.
결과물 Preview
간단한 프로젝트니 섬세한 로그인 관리는 만들지 않았다. 단순하게
1. 계정이 존재하지 않는다면 : '계정 생성' -> '지갑 발급' -> '로그인'
2. 계정이 존재한다면 : '로그인'
3. 계정이 존재 & 패스워드 불일치 : 로그인 재시도 필요
4. 'server' 계정으로 접속 시 : 관리자 페이지로 이동
콤포넌트
위에서 보다시피 클라이언트 Component 는 크게 4가지로 구성하였다.
- 커뮤니티 (글 관리)
- 토큰
- NFT
- 마이페이지
접속을 한번 해보겠다.
CSS 는 전혀 1도 꾸민 것이 없으니 이해해 주시길 바란다 크흡;;
주어진 시간 동안 최대한 기능에 집중했다.
게시판(커뮤니티) 화면
각설하고, 첫 로그인 화면은 '커뮤니티' 화면이다.
- 게시판 글쓰기 기능
- 게시판 글 확인
그리고 위 Nav 바를 보면 접속한 계정의 정보를 확인할 수 있다.
- 이더리움 보유량
- 토큰 보유량
- 토큰 allowance 설정량
- 지갑주소
- 계정명, 가입일..
토큰 화면
'Token' 콤포넌트에 접속해보자.
토큰과 관련된 행동을 할 수 있는 곳이다.
- 다른 사용자에게 토큰 전송
- 이더리움 받기
- 내 토큰 거래 내역 확인
토큰 전송은 transfer 를 호출한다. 그리고 이더리움 받기(EthFaucet) 는 가나슈 가상 계정에서 받아온다.
한편 '토큰 거래 내역' 은 블록에서 direct 로 긁어오는 것이 아니라, 별도의 데몬을 통해 mysql 에 주기적으로 적재되는 데이터로부터 호출한다. 이는 속도 측면에서 매우 효율적이다.
그리고 개인적으로 이 트랜잭션 내역을 manage 하는 것이 이번 프로젝트에서 가장 힘든 일 중 하나였다. 받는 사람(to) 와 전송 토큰 수량(tokenAmount) 을 확인하는 법을 몰랐었기 때문이다.
NFT 화면
NFT 는 아직 구현을 못한 부분이 많기 때문에 아쉽다. 이용자가 보유한 NFT 및 커뮤니티 전체 NFT 리스트가 보여야하는데 시간 상 진행하지 못했다.
이번 프로젝트에서 NFT 를 생성하기 위해서 이용자는 두 번의 과정을 거쳐야 한다.
1. 이용자의 일정 토큰을 서버 계정에 approve (돈 묶어두기)
2. NFT 민팅 시 서버 계정에 할당되어 있는 allowance 를 tranferFrom (결제)
위의 1번 과정이 화면에서의 'NFT 민팅에 사용될 금액 충전하기' 이다.
그리고 'NFT 생성하기' 에서 이미지 URI 를 넣고 mint 하면 2번 과정이 실행된다.
마이페이지
마이페이지에서는 이용자와 관련된 트랙잭션 정보(토큰 및 NFT 의 전송/구매 내역 등) 를 보여주기 위해 야심차게 만든 콤포넌트지만 시간 상 진행을 하지 못했다.
게시판에서 '내가 쓴 글', '보상 내역' 등도 포함시켜야 할 것이다.
관리자 페이지
'server' 계정으로 접속하면 간단한 관리자 페이지를 보여주려 했다.
사실 이번 프로젝트의 핵심은 블록체인의 컨트랙트 및 사용자가 보유한 토큰/NFT 를 'server' 가 통제 관리 한다는 측면에 있다.
따라서 서버(관리자) 는 마음대로 컨트랙트를 수정할 수 있으며, 뭐 토큰 에어드랍도 마음대로 하고 사용자의 토큰(또는 NFT) 을 마음대로 소유권 변경할 수도 있다.
그러한 측면을 관리자 페이지에 담고 싶었다.
특히 가스 수수료 및 성능을 개선하기 위해 이용자들의 트랜잭션 요청을 로컬 DB에 별도로 모아두었다가, 적정량이 쌓이면 한번의 트랜잭션으로 요청을 모두 처리하는 API 를 개발해 두었다. 이런 기능을 모두 관리자 페이지 클라이언트에 적용할 수 있다. (예를 들면 많은 사용자에게 한번에 토큰 에어드랍을 수행하는 것도 하나의 트랜잭션으로 가능할 것이다)
이렇게 하고 싶은 계획들은 아무 많았지만 아직 완성을 못했다. (서버 측은 개발하였으나 클라이언트 측 미완성)
아쉽다!!
지금부터는 실제 구현을 어떻게 했는지 소스를 확인토록 하겠다.
백앤드 server 측 개발 과정
소스의 틀은 아래와 같다.
/controller 리렉토리에 실제 RESTful API 를 모두 만들어 두었다.
예를 들어 어려웠던 API 하나만 예시로 설명해 보겠다.
다음은 /contract/mintToken 이라는 API 코드이다.
mintToken: async (req, res) => {
console.log(`======== [POST] /contract/mintToken ========`);
const TOKEN_VALUE = req.body.tokenAmount || 10;
//계정 정보
let serverAddress, serverPrivKey, userAddress, userPrivKey;
const reqUserName = req.body.userName;
const reqPassword = req.body.password;
console.log(`${reqUserName} ${reqPassword} ${TOKEN_VALUE}`);
//서버, 사용자 지갑 호출
try {
const serverAccount = await dbC.getServerAccount();
if (serverAccount.length > 0) {
serverPrivKey = serverAccount[0].privateKey;
serverAddress = serverAccount[0].address;
} else {
res.status(409).json({ message: "Error: server account not exist or password invalid" });
}
const userAccount = await dbC.getUserAccount(reqUserName, reqPassword);
if (userAccount.length > 0) {
userPrivKey = userAccount[0].privateKey;
userAddress = userAccount[0].address;
} else {
res.status(409).json({ message: "Error: user account not exist or password invalid" });
}
} catch (e) {
console.log(e);
return res.status(502).json({ message: e });
}
//스마트 컨트랙트 객체를 생성
const contractObj = new web3.eth.Contract(abi, contractAddress, {
from: serverAddress,
gasPrice: "20000000",
});
//보낼 트랜잭션 데이터 생성
const data = contractObj.methods.mintToken(userAddress, contractAddress_ERC721, TOKEN_VALUE).encodeABI(); //Create the data for token transaction.
try {
let userBalance = await contractObj.methods.balanceOf(userAddress).call();
console.log(`======== 토큰 발송 전 보유량 ========`);
console.log(`[전송 전] user 토큰 수 : ${userBalance}`);
//트랜잭션에 server 서명
const signedTx = await web3.eth.accounts.signTransaction(
{
to: contractAddress, //받는 사람이 아니라, 계약주소임. 받는 사람은 transfer 함수에 설정
gas: 100000,
data: data,
},
serverPrivKey
);
//트랜잭션 발송
const rtnTran = await web3.eth.sendSignedTransaction(signedTx.rawTransaction);
userBalance = await contractObj.methods.balanceOf(userAddress).call();
//let allowance = await contractObj.methods.allowance(userAddress, serverAddress).call();
let allowance = await contractObj.methods.allowance(userAddress, contractAddress_ERC721).call();
console.log(`======== 트랜잭션 결과(토큰 발행 및 apporve [user][server]) ========`);
console.log(`[전송 후] to 토큰 수 : ${userBalance}`);
console.log(`[전송 후] allowance[user][server] 토큰 수 : ${allowance}`);
//사용자 토큰 수를 DB 에 업데이트
let sql = "UPDATE users SET token = ? WHERE userName = ?";
await dbC.queryPromise(sql, [userBalance, reqUserName]);
res.status(200).json({
message: "mintToken Successed",
data: {
username: reqUserName, // 사용자 이름
address: userAddress, // 받는 계정의 주소
txHash: rtnTran.transactionHash, // 트랜잭션 해시
tokenBalance: userBalance, // 유저의 토큰 잔액
},
});
} catch (e) {
console.log(e);
res.status(502).json({
message: "Error: signTransaction Failed",
});
}
},
크게 과정은 다음과 같다.
1. 클라이언트로부터 대상 이용자의 지갑주소와 mint할 토큰 수량을 가져온다.
2. server 계정의 개인키를 추출한다.
3. 컨트랙트 객체 생성
4. mintToken 함수 데이터 생성
5. server 개인키로 트랜잭션 서명
6. 트랜잭션 발송
이 때 ERC20 의 mintToken 이라는 함수는 이용자에게 토큰을 지급하고, 그 지급된 양만큼의 토큰을 allowance[이용자][서버지갑] 으로 설정해 둔다.
그런데 문제는 이렇게 설정해둔 allowance[이용자][서버지갑] 을 나중에 mintNFT 라는 ERC721 함수에서 이용해야 되는데, ERC721 함수에서 호출을 하면 이상하게 해당 allowance 가 확인이 되지 않는 것이었다.
알고 보니 ERC721 이 ERC20 을 호출할 때는 msg.sender 값이 내가 기대했던 'server 주소' 가 아니라, 실제로 ERC220 을 호출하는 ERC721 Contract 주소로 잡히는 것이었다!!
이 그림을 보면 조금 더 이해가 될 것이다.
따라서 위의 mintToken API 에서 allowance[이용자][서버지갑] 을 설정하는 것이 아니라, allowance[이용자][ERC721 CA] 로 설정해주어야 했다.
백앤드 데몬
우리가 항상 블록체인으로부터 데이터를 추출하는 것은 여러모로 불편하다. 항상 모든 블록을 다 뒤져서 트랜잭션을 찾아내는 것은 속도 면에서 너무 느리다.
따라서 별도의 오프체인 데이터를 구성하는 것이 좋다.
그것을 위해 특정 시간마다 블록에서 최신 데이터들을 가져와 mysql DB 에 적재하는 기능이 소스에 업로드해 둔 Daemon 이다.
현재는 ERC20만 적재하도록 되어 있다. 시간이 된다면 ERC721 도 적재하도록 하겠다.
꿀팁이 있다면, DB 에 토큰 관련 트랜잭션을 적재해 둘 때에는 'input' 값을 아래와 같이 나눠서 별도의 컬럼들에 저장해 두는 것이 좋다.
//받는 사람 및 토큰 갯수 정리
let toAddress = String(tran.input).substring(34,74);
//let tokenAmount = String(tran.input).substring(74,138);
let tokenAmount = String(tran.input).substring(128,138);
tran.toAddress = '0x' + Web3.utils.toChecksumAddress(toAddress);
tran.tokenAmount = parseInt(tokenAmount, 16);
왜냐하면 트랜잭션 정보에는 토큰의 '받는 사람 주소' 및 '토큰 수량' 이 없다.
그 정도는 input 에 있다. 따라서 input 을 잘라서 필요한 정보를 추출해 내야 한다.
- 첫 번째 34 자리 : 함수 서명 (34 bits)
- 두 번째 40 자리 : 받는 사람 지갑 주소 (256)
- 세 번째 64 자리 : 토큰 수량
근데 가끔 위 정보와 다른 경우도 있는 것 같다. 이는 ERC20 함수에 따라 다른 것 같다. 위 기준은 ERC20 의 transfer() 함수에 해당한다.
개발 회고
추천 받은 회고 방법론을 통해 지금까지 한 내용을 정리해 보겠다.
회고 방법론 : KPT (KEEP, PROBLEM, TRY)
Keep (장점, 유지할 점) :
개인적으로 정말 열심히 했다고 생각한다. 이번 프로젝트에서는 '빈 껍데기' 부터 차근히 코드를 짜려고 노력했다. 내가 짜둔 것을 복붙하거나 다른 사람의 코드를 수정하는 등을 하지 않았다.
그러다보니 코드 설계를 더 체계적으로 할 수 있었고, 불필요한 코드 라인이 발생하지 않았다. 변수명도 확실히 올바르게 만들 수 있었다.
또한 다른 분들과 함께 프로젝트를 하니까 자연스럽게 Github 사용법에 조금 익숙해지게 되었다.
이처럼 '체계적으로 틀을 짜고 코드의 구조를 만드는' 습관을 들이고, 유지하도록 해야 겠다.
Problem (단점, 변경 또는 버릴 점) :
프로젝트를 진행하면서 '최대한 팀원에 맞춰주자' 라는 마인드로 다른 팀원들과 불화를 일으키지 않으려고 노력했다.
그러다보니 정작 '소리를 내야 할 때' 를 놓치곤 하는 것 같다.
one team 으로 더 잘해내기 위해서는 필요할 때 나의 의견을 더 피력해야 한다.
또한 내가 다른 분들에 비해 나이가 있다보니, 총대를 매거나 내가 뭔가 상황을 정리해야 하는 상황이 있는데 소위 나잇값? 을 못한 것 같기도 하다.
다 참는 것도 좋지만, 때로는 팀을 위해서 싫은 소리? 도 할 줄 아는 자세를 가져야 겠다.
Try (시도할 점, 앞으로의 행동) :
프로젝트의 남은 부분을 정말 끝마치고 싶다.
내일부터는 또 다른 프로젝트의 시작이라 말은 그렇게 해도 다시 안할 것 같다.
하지만!!!
정말 시간을 내서 프로젝트의 이루지 못한 부분들을 마무리 짓겠다.
디자인 측면도 적용(CSS) 해서 예쁘고, 기능적으로도 완벽한 하나의 애플리케이션으로 완성하고 싶다.
그래서 포트폴리오에도 추가할 것이다.
읽어주셔서 감사합니다.
Author And Source
이 문제에 관하여(Token 기반 커뮤니티 만들기), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@jihonee/Token-기반-커뮤니티-만들기저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)