일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | ||||
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 |
- #유니티
- http 모듈
- Factory 함수
- ES6 모듈
- 불안정 정렬
- npm
- 텍스트 가운데 정렬
- Hybrid Blockchain
- IP
- CSS Specificity
- solidity
- UTXO
- #1인게임개발
- 블록체인
- 3티어 아키텍처
- short-circuiting
- Factory Functions
- NoSQL
- CSS
- 안정 정렬
- react
- skip ci
- SQL
- CLI
- 명시도
- Relational Database
- javascript
- caverjs
- Private Blockchain
- 2티어 아키텍처
- Today
- Total
짹뚜 스튜디오
[프로젝트] 토큰 보상 커뮤니티 (Second Life Say) 본문
이번에 진행한 프로젝트는 커뮤니티 활동에 참여를 하면 보상으로 토큰을 지급하고 획득한 토큰으로 NFT를 구입할 수 있는 서비스이다. 팀원들과 모여서 가장 먼저 한 일은 우리의 서비스에 어떠한 기능들을 추가할 것인지 서로 의견을 나눴고 다음과 같이 결론이 나왔다.
- 글을 작성하면 토큰을 지급한다.
- 글에 좋아요를 누르면 글 작성자에게 토큰을 지급한다.
- 댓글을 달면 댓글을 단 사람에게 토큰을 지급한다.
- 회원가입 시 토큰을 지급한다.
- 자신이 만든 글들을 엮어서 출판할 수 있는 쿠폰을 NFT 형태로 판매한다.
- NFT를 구매할 때는 토큰을 사용한다.
클라이언트에서 서버로 API 요청을 하면 서버는 DB에서 정보를 가져와서 클라이언트에 데이터를 보내준다. 그런데 서버에서는 DB에서 데이터를 가져오는 것과 동시에 특정 API 요청에서는 토큰을 민팅하는 트랜잭션을 블록체인에 보내야 한다. 만약 트랜잭션이 confirm 될 때까지 서버가 기다린다면 사용자 또한 그 시간 동안 아무것도 못하고 기다리기만 해야 할 것이다. 이것을 해결하기 위해 daemon 프로그램을 구현해서 블록체인의 블록을 특정 시간마다 확인을 해서 서버에서 보낸 transaction이 블록에 담겨 있는지 확인을 하도록 만들었다. 이렇게 되면 사용자는 딜레이 없이 서비스를 이용할 수 있게 된다.
프로젝트 데모 영상
내가 맡은 백엔드 부분은 밑에서 설명할 것이고 전체 코드는 깃허브에서 확인할 수 있다.
https://github.com/JJakDDo/beb-03-ABDO
서버 계정 생성
일단 우리 프로젝트는 서버 계정이 하나 있고 그 계정을 통해 모든 트랜잭션을 보낼 것이다. 왜냐하면 이번 프로젝트의 목표 중 하나가 사용자에게 Web2.0의 일반적인 커뮤니티 서비스와 동일한 UX를 느끼게 하는 것이기 때문이다. 그렇게 하려면 사용자가 메타마스크를 통해 지갑을 연결하고 트랜잭션을 보내는 것이 아닌 서버 계정의 지갑에서 모든 트랜잭션을 보내서 토큰도 각 사용자에게 지급할 수 있어야 한다.
서버 계정은 서버가 시작할 때 web3 모듈을 통해서 생성이 된다. 생성된 지갑은 DB에 저장이 된다. DB는 mongoDB를 활용했다.
컨트랙트 배포
서버 계정을 만든 후 가장 먼저 해야 할 일이 ERC20과 ERC721 컨트랙트를 배포하는 일이다. 그러나 방금 만든 서버 계정에는 ETH가 없기 때문에 ETH를 보내줘야 한다. 서비스를 배포할 때는 직접 ETH를 보내는 방식을 선택했지만, 테스트를 진행하는 동안은 Ganache 환경에서 진행하기 때문에 Ganache의 첫 번째 계정에서 자동으로 ETH를 보내게 구현했다.
ETH를 받은 후에 컨트랙트들을 서버 계정이 배포하게 구현을 했다. 트랜잭션을 보내기 전에 먼저 트랜잭션을 서버 계정의 비밀키로 서명을 해야 한다. 왜냐하면 우리가 연결할 블록체인 노드 (Infura 등등)들은 서버 계정의 비밀키를 모르기 때문이다. 그래서 우리가 보낼 함수의 bytecode를 가져오고 그것을 nonce, gas, gasPrice와 같이 객체로 만들어 준다음 web3의 signTransaction 함수를 사용해서 서명을 한다. 그렇게 되면 반환 값으로 rawTransaction을 받아오는데, 이제 이것을 web3의 sendSignedTransaction의 인자값으로 보내준다. 이렇게 하면 트랜잭션을 보낼 수가 있다. 컨트랙트 배포의 경우에는 contract 주소값을 반환값으로 받아올 수 있다.
const nonce = await web3.eth.getTransactionCount(
data.address,
"pending"
);
const gasPrice = await web3.eth.getGasPrice();
let contract = new web3.eth.Contract(abi);
const bytecodeWithEncodedParameters = contract
.deploy({
data: bytecode,
})
.encodeABI();
const gasLimit = await web3.eth.estimateGas({
data: bytecodeWithEncodedParameters,
from: data.address,
gasPrice: web3.utils.toHex(gasPrice),
});
const txObject = {
nonce: web3.utils.toHex(nonce),
gasLimit: web3.utils.toHex(gasLimit),
gasPrice: web3.utils.toHex(gasPrice),
data: `0x${bytecode}`,
};
const { rawTransaction } = await web3.eth.accounts.signTransaction(
txObject,
data.privateKey
);
const { contractAddress } = await web3.eth.sendSignedTransaction(
rawTransaction
);
이렇게 받아온 contract 주소들을 DB에 저장한다.
글 작성
API를 호출하면 글 정보들을 DB에 저장하고 해당 사용자에게 5 토큰을 지급한다.
POST /writing
Required Headers
Authorization: Bearer [Access Token]
Content-Type: application/json
Required Body
{
"title": "Writing's Title",
"content": "Writing's Content"
}
Response
STATUS 201
{
"status": "success",
"data": {
"writingId": "Writing's Object ID"
}
}
STATUS 400
{
"status": "fail",
"message": "Title, content or nickname is missing"
}
모든 글 가져오기
API를 호출하면 DB에 저장된 모든 글을 가져온다.
GET /writing
Response
STATUS 200
{
"status": "success",
"data": [
{
"id": "Writing's Object ID",
"title": "Writing's Title",
"content": "Writing's Content",
"writer": "Writer's ID",
"nickname": "Writer's Nickname",
"comments": [
{
"userId": "Commenter's ID",
"nickname": "Commenter's Nickname",
"comment": "Comments"
}
],
"likes": [
"Liker's ID"
],
"createdAt": "2022-04-21T05:22:05.709Z"
}
]
}
특정 글 가져오기
API를 호출하면 원하는 글만 가져온다.
GET /writing/:id
:id는 db에 저장된 글의 object ID이다.
Response
STATUS 200
{
"status": "success",
"data": {
"id": "Writing's Object ID",
"title": "Writing's Title",
"content": "Writing's Content",
"writer": "Writer's ID",
"nickname": "Writer's Nickname",
"comments": [
{
"userId": "Commenter's ID",
"nickname": "Commenter's Nickname",
"comment": "Comments"
}
],
"likes": [
"Liker's ID"
],
"createdAt": "2022-04-21T07:49:47.675Z"
}
}
STATUS 400
{
"status": "fail",
"message": `Writing ID: ${id} does not exist!`
}
댓글 요청
API를 호출하면 해당 글에 댓글을 요청한다. 정상적인 요청이라면 댓글 작성자에게 토큰 1개를 지급한다.
POST /writing/comment
Required Headers
Authorization: Bearer [Access Token]
Content-Type: application/json
Required Body
{
"writingId": "Writing's Object ID",
"comment": "Comment"
}
Response
STATUS 200
{
"status": "success"
}
좋아요 요청
API를 호출하면 해당 글에 좋아요를 요청한다. 정상적인 요청이라면 글 작성자에게 토큰 1개를 지급한다. 만약 이미 좋아요를 한 상태라면 에러 응답을 보낸다.
POST /writing/like
Required Headers
Authorization: Bearer [Access Token]
Content-Type: application/json
Required Body
{
"writingId": "Writing's Object ID"
}
Response
STATUS 200
{
"status": "success"
}
STATUS 400
{
"status": "fail",
"message": "Cannot send like again"
}
미들웨어 (jwt 확인)
특정 요청은 header에 토큰이 있어야 한다. 서버에서 요청에 토큰이 있는지 확인하기 위해서 미들웨어를 구현했다. 미들웨어는 authorization header가 있는지 확인하고 Bearer 토큰인지 확인한다. 그런 다음에 jsonwebtoken 모듈의 verify 함수를 사용해서 토큰이 유효한지 확인을 한다. 만약 토큰이 유효하지 않다면 에러를 보낸다.
// 헤더에 토큰이 없을 때
STATUS 401
{
"status": "fail",
"message": "Not Authorized!"
}
// 토큰이 유효하지 않을 때
STATUS 401
{
"status": "fail",
"message": "Token is invalid!"
}
미들웨어 (에러 핸들링)
요청에 대한 에러들을 유지보수가 쉽게 하기 위해서 커스텀 에러 클래스와 미들웨어를 구현했다. 커스텀 에러 클래스는 Error를 상속받아서 구현했다.
export default class CustomError extends Error {
constructor(msg, statusCode) {
super(msg);
this.status = "fail";
this.statusCode = statusCode;
}
// 요청이 잘못되었을 때 에러 응답
static BadRequest = (msg) => {
return new CustomError(msg, 400);
};
// 토큰이 잘못되었을 때 에러 응답
static Unauthenticated = (msg) => {
return new CustomError(msg, 401);
};
// 요청에 대한 권한이 없을 때 에러 응답
static Forbidden = (msg) => {
return new CustomError(msg, 403);
};
// Not Found
static NotFound = (msg) => {
return new CustomError(msg, 404);
};
}
그리고 미들웨어 함수에서는 발생한 에러가 CustomError의 인스턴스인지를 확인하고, 만약 맞다면 해당 에러의 Status Code와 메시지를 함께 보내고 만약 아니라면 서버 에러를 보낸다. 그래서 에러 응답을 해야 할 때는 다음과 같이 코드를 작성하면 된다.
throw CustomError.BadRequest("Title, content or nickname is missing");
여기서 문제가 router의 controller 가 비동기 함수로 되어있다면 express의 기본 에러 처리 방식으로 비동기 함수의 에러를 처리하지 못한다. 그래서 그냥 throw를 해도 에러를 처리하지 못한다. 그래서 해결 방법으로는 wrapper 함수를 구현하거나 trycatch로 묶는 방법이 있지만, 가장 쉬운 방법은 express-async-error 모듈을 설치하는 것이다. 이 모듈을 설치하게 되면 비동기 함수에서도 throw로 에러를 처리할 수 있다.
이번 프로젝트를 진행하면서 백엔드에서 API를 구현하는 방법에 대해 많이 공부하게 되었고, 미들웨어 구현도 공부할 수 있는 기회가 되어서 좋았다. 팀원분들이 다들 맡은 부분들을 잘해주셔서 처음에 기획한 의도대로 시간 내에 모두 구현이 되어서 만족할만한 결과물이 나온 거 같다. 이번 프로젝트에서는 클라이언트 구현을 하지않았기 때문에 나중에 시간이되면 프론트도 공부할 겸 직접 구현할 예정이다.
'개발 공부 > 블록체인' 카테고리의 다른 글
[프로젝트] Opensea 클론코딩 (0) | 2022.04.18 |
---|---|
ICO 스마트 컨트랙트 구현하기 (0) | 2022.04.07 |
caver-js-ext-kas 를 활용한 클레이튼 API 구현 (0) | 2022.04.06 |
[암호화폐] 채굴 (0) | 2022.03.07 |
[암호화폐] UTXO (0) | 2022.03.03 |