OpenSea 클론 코딩 프로젝트

윤장원·2022년 8월 16일
0

프로젝트

목록 보기
1/2

첫 번째 프로젝트는 NFT 거래소, OpenSea의 클론코딩이다. 사용자가 NFT를 생성, 저장, 구매 및 판매할 수 있는 플랫폼을 만들어 보았다.

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

구현 목표

  • OpenSea API를 이용하여 현재 등록된 NFT, Collection을 조회하고 해당 NFT에 대한 정보를 볼 수 있다.
  • 사용자가 NFT 이미지, 정보를 입력하여 민팅을 하고 NFT를 저장할 수 있다.
  • 자신의 NFT를 다른 사람에게 전송할 수 있다.
  • 자신의 NFT를 판매하거나 다른 사람의 NFT를 구매할 수 있다.

구성 페이지

  • 처음 사이트에 접속했을 때 보이는 Main
  • 등록된 NFT, Collection을 볼 수 있는 Explore
  • NFT를 민팅할 수 있는 Create
  • 자신의 NFT 목록을 보고 판매 및 다른 사람에게 전송할 수 있는 MyPage
  • 지갑을 연결하고 연결된 계정의 잔고를 볼 수 있는 Sidebar

역할 분담

프로젝트는 3명이서 진행했다. 나는 서버와 DB를 맡게 되었고, 다른 분들은 각각 프론트엔드와 스마트컨트랙트를 맡으셨다.
DB는 MySQL을 이용하여 NFT와 사용자 계정의 정보를 관리할 수 있도록 했고, 서버에서는 OpenSea API를 이용해 NFT와 Collection 목록과 정보를 보내주었다.

관계형 데이터베이스 구현

테이블 구성은 다음과 같다. NFT 정보를 담는 NFT 테이블, Collection의 정보를 담는 Collections 테이블, NFT의 거래 기록을 담는 History 테이블, NFT 스마트 컨트랙트 정보를 담는 Contracts 테이블, 그리고 유저의 정보를 담는 User 테이블이 있다.

schema.sql

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

CREATE TABLE Contracts (
  contract_address varchar(100),
  contract_description varchar(500),
  contract_title varchar(100),
  contract_category varchar(100),
  created_at datetime DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY(contract_address)
);

CREATE TABLE History (
  history_id INT AUTO_INCREMENT,
  token_id INT,
  history_token_price varchar(200),
  from_address varchar(100),
  to_address varchar(100),
  transaction_hash varchar(100),
  transaction_time datetime,
  created_at datetime DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY(history_id)
);

CREATE TABLE Collections (
  collection_no INT,
  collection_name varchar(100),
  collection_key varchar(100),
  collection_profile_img varchar(200),
  collection_banner_img varchar(200),
  collection_description varchar(500),
  collection_author varchar(100),
  created_at datetime DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY(collection_no)
);

CREATE TABLE Users (
  user_no INT AUTO_INCREMENT,
  user_account varchar(100),
  user_balance varchar(200),
  user_chain varchar(100),
  created_at datetime DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY(user_no)
);

ALTER TABLE History ADD FOREIGN KEY (token_id) REFERENCES NFT (token_id);
ALTER TABLE NFT ADD FOREIGN KEY (contract_address) REFERENCES Contracts (contract_address);
ALTER TABLE NFT ADD FOREIGN KEY (collection_no) REFERENCES Collections (collection_no);

서버 API 구현

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

OpenSea API를 이용하여 서버에 요청을 보내면 요청을 보내는 주소와 파라미터에 따라 필요한 정보를 보내주도록 했다.

1. NFT 무작위로 원하는 개수로 조회

Request
Get /opensea

Parameter

ParameterTypeDescriptionNecessary
limit숫자조회하고 싶은 NFT의 수필수

ex)/opensea?limit=8

Response
응답은 다음과 같은 JSON 형식이다.

[
    {
        "id": 583254620,
        "num_sales": 0,
        "background_color": null,
        "image_url": "https://lh3.googleusercontent.com/ixvxtnJO76dwvSlBU3g2ho5gcbK01bBzNSuZMv-iQ23-lmUsBp3HFJ82LhDDf8Wegrs1u236zNnseQRYKH6YwyA-0ZL0b5M_tQsj",
        "image_preview_url": "https://lh3.googleusercontent.com/ixvxtnJO76dwvSlBU3g2ho5gcbK01bBzNSuZMv-iQ23-lmUsBp3HFJ82LhDDf8Wegrs1u236zNnseQRYKH6YwyA-0ZL0b5M_tQsj=s250",
        "image_thumbnail_url": "https://lh3.googleusercontent.com/ixvxtnJO76dwvSlBU3g2ho5gcbK01bBzNSuZMv-iQ23-lmUsBp3HFJ82LhDDf8Wegrs1u236zNnseQRYKH6YwyA-0ZL0b5M_tQsj=s128",
        "image_original_url": null,
        "animation_url": null,
        "animation_original_url": null,
        "name": "Proud Lions Club #1378",
        "description": null,
        "external_link": null,
        "asset_contract": {
            "address": "0x495f947276749ce646f68ac8c248420045cb7b5e",
            "asset_contract_type": "semi-fungible",
            "created_date": "2020-12-02T17:40:53.232025",
            "name": "OpenSea Collection",
            "nft_version": null,
            "opensea_version": "2.0.0",
            "owner": 458910490,
            "schema_name": "ERC1155",
            "symbol": "OPENSTORE",
            "total_supply": null,
            "description": "",
            "external_link": null,
            "image_url": "https://openseauserdata.com/files/860b94bee079e3864d04849383d2b4d1.bin",
            "default_to_fiat": false,
            "dev_buyer_fee_basis_points": 0,
            "dev_seller_fee_basis_points": 0,
            "only_proxied_transfers": false,
            "opensea_buyer_fee_basis_points": 0,
            "opensea_seller_fee_basis_points": 250,
            "buyer_fee_basis_points": 0,
            "seller_fee_basis_points": 250,
            "payout_address": null
        },
        "permalink": "https://opensea.io/assets/ethereum/0x495f947276749ce646f68ac8c248420045cb7b5e/23739362589833214218042992734890100206674559438404508682768786642835142082561",
        "collection": {
            "banner_image_url": "https://openseauserdata.com/files/873b29124522c65ba58454c5fd038c3a.jpg",
            "chat_url": null,
            "created_date": "2022-08-11T14:52:24.093013",
            "default_to_fiat": false,
            "description": "A collection of warrior Lions and Lionesses. The NFTs will be your pass into the Lionverse Metaverse and also enjoy Travel discounts. Holders can earn daily staking rewards, morph and breed their Lions to upgrade stats.\nWe are building our metaverse (Lionverse), which will support our native 'Steak' tokens, holders can play our P2E games and earn big rewards. The Lionverse will, have a number of mini play to earn games as well as lands for other projects, which they can customized and integrated with their own NFTs. Our lion holders will benefit from the land sales and upgrades revenue via the 'Steak' tokens.The Lionverse Alpha is already deployed and holders can enjoy AMAs and game events weekly!",
            "dev_buyer_fee_basis_points": "0",
            "dev_seller_fee_basis_points": "0",
            "discord_url": null,
            "display_data": {
                "card_display_style": "contain"
            },
            "external_url": null,
            "featured": false,
            "featured_image_url": "https://openseauserdata.com/files/873b29124522c65ba58454c5fd038c3a.jpg",
            "hidden": false,
            "safelist_request_status": "not_requested",
            "image_url": "https://openseauserdata.com/files/e8b9fdbc00c46cd5024929379f6fcca5.png",
            "is_subject_to_whitelist": false,
            "large_image_url": "https://openseauserdata.com/files/873b29124522c65ba58454c5fd038c3a.jpg",
            "medium_username": null,
            "name": "Lions Official Proud Club",
            "only_proxied_transfers": false,
            "opensea_buyer_fee_basis_points": "0",
            "opensea_seller_fee_basis_points": "250",
            "payout_address": null,
            "require_email": false,
            "short_description": null,
            "slug": "lions-official-proud-club",
            "telegram_url": null,
            "twitter_username": null,
            "instagram_username": null,
            "wiki_url": null,
            "is_nsfw": false
        },
        "decimals": null,
        "token_metadata": null,
        "is_nsfw": false,
        "owner": {
            "user": {
                "username": "NullAddress"
            },
            "profile_img_url": "https://storage.googleapis.com/opensea-static/opensea-profile/1.png",
            "address": "0x0000000000000000000000000000000000000000",
            "config": ""
        },
        "seaport_sell_orders": null,
        "creator": {
            "user": {
                "username": null
            },
            "profile_img_url": "https://storage.googleapis.com/opensea-static/opensea-profile/12.png",
            "address": "0x347c00c950eac5380361cc9ccc529e5ffef291df",
            "config": ""
        },
        "traits": [],
        "last_sale": null,
        "top_bid": null,
        "listing_date": null,
        "is_presale": true,
        "transfer_fee_payment_token": null,
        "transfer_fee": null,
        "token_id": "23739362589833214218042992734890100206674559438404508682768786642835142082561"
    }
]

2. 하나의 NFT 정보 조회

Request
GET /opensea

Parameter

ParameterTypeDescriptionNecessary
contractaddress문자열해당 NFT의 컨트랙트 주소필수
tokenId문자열해당 NFT의 tokenId필수

ex)/opensea?contractaddress=0xb47e3cd837ddf8e4c57f05d70ab865de6e193bbb&tokenId=1

Response
응답은 해당 NFT의 정보를 담은 JSON의 형식이다.

3. 원하는 Collection의 NFT를 원하는 수 만큼 조회

Request

GET /opensea/onlycollections

Parameter

ParameterTypeDescriptionNecessary
limit숫자조회하고 싶은 NFT의 수필수
collection_slug문자열collection slug필수

ex)/opensea/onlycollections?limit=10&collection_slug=clonex

Response

응답은 해당 Collection에 속하는 NFT들의 정보를 담은 JSON 형식입니다.

4. NFT Collections 조회

Request
GET /opensea

Parameter

ParameterTypeDescriptionNecessary
collectionoffset문자열offset 숫자필수
collectionlimit문자열Maximum 숫자필수

ex)/opensea?collectionoffset=0&collectionlimit=10

Response
응답은 Collection들의 정보를 담은 JSON 형식입니다.

5. 하나의 NFT Collection 조회

Request

GET /opensea

Parameter

ParameterTypeDescriptionNecessary
collecionslug문자열collection slug필수

ex)/opensea?collecionslug=abc

Response

응답은 해당 Collection의 정보를 담은 JSON 형식입니다.

6. NFT 보유 목록 조회

Request

GET /explore

Parameter

ParameterTypeDescriptionNecessary
account문자열해당 계정이 소유하고 있는 NFT 조회필수 아님
name문자열해당 이름에 대응하는 NFT 조회필수 아님

ex)
/explore?account=0x...
/explore?name=...

parameter가 없는 GET 요청은 서버에 등록되어 있는 모든 NFT를 조회합니다.

Response

응답은 다음과 같은 JSON 형식입니다.

[
    {
        "token_id": 1,
        "token_img": "이미지 주소",
        "token_name": "토큰 이름",
        "token_owner": "owner 계정",
        "token_description": "description",
        "token_price": 2.3
    }
]

메세지에서 사용하는 속성은 다음과 같습니다.

parameterTypeDescription
token_id숫자발급된 NFT의 고유한 tokenId
token_img문자열NFT의 이미지 링크
token_name문자열NFT의 이름
token_owner문자열NFT를 소유하고 있는 주소
token_description문자열NFT에 대한 설명
token_price숫자NFT의 가격

7. 지갑 연결 시 계정 정보 저장

Request

POST /connect

요청 형식 : JSON

Parameter

ParameterTypeDescriptionNecessary
user_account문자열해당 유저의 계정필수
user_balance문자열해당 유저의 잔고필수
chain문자열해당 유저의 네트워크 체인필수

1) 새로운 계정으로 지갑 연결 했을 경우 : 연결한 유저의 계정, 잔고, 네트워크 체인 정보를 데이터베이스에 추가합니다.
2) 계정을 다른 네트워크 체인에 연결할 경우 : 연결한 유저의 계정, 잔고, 네트워크 체인 정보를 데이터베이스에 추가합니다.
3) 현재 계정의 잔고가 변할 경우 : 변경된 잔고 정보를 기존 데이터베이스에 업데이트 합니다.

server/index.js

const express = require('express');
const cors = require('cors');
const app = express();
const port = 4000;

const exploreRouter = require('./router/explore');
const connectRouter = require('./router/connect');
const openseaRouter = require('./router/opensea');

app.use(cors({ origin: '*' }));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));

app.use('/explore', exploreRouter);
app.use('/connect', connectRouter);
app.use('/opensea', openseaRouter);

app.get('/', (req, res) => {
  res.status(200).send('NFT Exchange open');
});

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

module.exports = app;

server/controller/index.js

const models = require('../models');
const axios = require('axios');

module.exports = {
  findAll: (req, res) => {
    if (req.query.address != undefined) {
      models.get1(req.query.address, (error, result) => {
        if (error) {
          return res.status(500).send('Internal Server Error');
        } else {
          return res.status(200).json(result);
        }
      });
    } else if (req.query.name != undefined) {
      models.get2(req.query.name, (error, result) => {
        if (error) {
          return res.status(500).send('Internal Server Error');
        } else {
          return res.status(200).json(result);
        }
      });
    } else {
      models.get3((error, result) => {
        if (error) {
          return res.status(500).send('Internal Server Error');
        } else {
          return res.status(200).json(result);
        }
      });
    }
  },
  saveAccount: (req, res) => {
    const { user_account, user_balance, chain } = req.body;
    console.log(req.body);

    models.saveAccount(user_account, user_balance, chain, (error, result) => {
      if (error) {
        return res.status(500).send('Internal Server Error');
      } else {
        return res.status(201).json({ message: 'success!' });
      }
    });
  },
  findNFT: async (req, res) => {
    if (req.query.limit != undefined) {
      const nftList = await axios
        .get(
          `https://api.opensea.io/api/v1/assets?order_direction=desc&offset=0&limit=${req.query.limit}`,
          {
            headers: {
              'x-api-key': '',
            },
          }
        )
        .then((response) => response.data.assets);
      console.log(nftList);
      return res.status(200).json(nftList);
    } else if (req.query.contractaddress != undefined && req.query.tokenId != undefined) {
      const nftList = await axios
        .get(
          `https://api.opensea.io/api/v1/asset/${req.query.contractaddress}/${req.query.tokenId}`,
          {
            headers: {
              'x-api-key': '',
            },
          }
        )
        .then((response) => response.data);

      return res.status(200).json(nftList);
    } else if (req.query.collecionslug != undefined) {
      const collections = await axios
        .get(`https://api.opensea.io/api/v1/collection/${req.query.collecionslug}`, {
          headers: {
            'x-api-key': '',
          },
        })
        .then((response) => response.data.collection);
      console.log(collections);
      return res.status(200).json(collections);
    } else if (req.query.collectionoffset != undefined && req.query.collectionlimit != undefined) {
      const collections = await axios
        .get(
          `https://api.opensea.io/api/v1/collections?offset=${req.query.collectionoffset}&limit=${req.query.collectionlimit}`,
          {
            headers: {
              'x-api-key': '',
            },
          }
        )
        .then((response) => response.data.collections);
      console.log(collections);
      return res.status(200).json(collections);
    } else if (req.query.collection_slug != undefined) {
      const collections = await axios
        .get(`https://api.opensea.io/api/v1/assets?collection_slug=${req.query.collection_slug}`, {
          headers: {
            'x-api-key': '',
          },
        })
        .then((response) => response.data.assets)
        .catch((err) => {
          console.log(err);
        });
      console.log(collections);
      return res.status(200).json(collections);
    }
  },
  findWantedNFT: async (req, res) => {
    if (req.query.limit != undefined && req.query.collection_slug != undefined) {
      const nftList = await axios
        .get(
          `https://api.opensea.io/api/v1/assets?order_direction=desc&offset=0&limit=${req.query.limit}&collection_slug=${req.query.collection_slug}`,
          {
            headers: {
              'x-api-key': '',
            },
          }
        )
        .then((response) => response.data.assets);
      console.log(nftList);
      return res.status(200).json(nftList);
    }
  },
};

server/models/index.js

const { default: axios } = require('axios');
const db = require('../db');

module.exports = {
  get1: (address, callback) => {
    const queryString = `SELECT token_id, token_img, token_name, token_owner, token_description, token_price FROM NFT WHERE token_owner = ?`;
    const params = [address];

    db.query(queryString, params, (error, result) => {
      console.log(result);
      callback(error, result);
    });
  },
  get2: (name, callback) => {
    const queryString = `SELECT token_id, token_img, token_name, token_owner, token_description, token_price FROM NFT WHERE token_name = ?`;
    const params = [name];

    db.query(queryString, params, (error, result) => {
      console.log(result);
      callback(error, result);
    });
  },
  get3: (callback) => {
    const queryString = `SELECT token_id, token_img, token_name, token_owner, token_description, token_price FROM NFT`;

    db.query(queryString, (error, result) => {
      console.log(result);
      callback(error, result);
    });
  },
  saveAccount: (user_account, user_balance, chain, callback) => {
    const getAccount = `SELECT user_account FROM Users WHERE user_account = "${user_account}" AND user_chain = "${chain}"`;
    const updateAccount = `UPDATE Users SET user_account="${user_account}", user_balance="${user_balance}", user_chain="${chain}" WHERE user_account="${user_account}" AND user_chain="${chain}"`;

    const insertAccount = `INSERT INTO Users (user_account, user_balance, user_chain) VALUES ("${user_account}","${user_balance}" , "${chain}")`;

    db.query(getAccount, (error, result) => {
      isAccount = JSON.stringify(result);
      console.log(isAccount);
      if (isAccount === '[]') {
        db.query(insertAccount, (error, result) => {
          callback(error, result);
        });
      } else {
        db.query(updateAccount, (error, result) => {
          callback(error, result);
        });
      }
    });
  },
};

프론트엔드

Main 페이지


Sidebar - 지갑을 연결할 수 있고, 연결하면 계정에 대한 정보를 보여준다.

Explore 페이지 - OpenSea에 등록된 Collection들을 보여준다.

NFT 상세 정보 - NFT를 클릭하면 해당 NFT에 대한 정보를 보여준다.

Create 페이지 - 이미지를 올리고 정보를 입력하면 NFT를 민팅할 수 있다.

프로젝트 회고

프론트엔드, 백엔드, 블록체인에 대한 공부를 하고 첫 프로젝트를 일주일이라는 짧은 기간 동안 진행했다. 서버 환경을 직접 만들어 보는 건 처음이라서 프로젝트 진행 과정에 여러 어려움들이 있었지만, 좋은 팀원들의 도움과 검색들을 통해서 해결해 나갈 수 있었다. 이번 프로젝트를 통해서 나의 부족한 점들을 알 수 있었고, 직접 사이트를 만들어 결과물을 보니 개발에 대한 흥미를 더욱 느낄 수 있었다.

0개의 댓글