상세 컨텐츠

본문 제목

OpenSea 클론코딩 프로젝트 회고

Blockchain/NFT

by Yongari 2023. 3. 27. 14:19

본문

Midjourney로 만든 그림

 

 

오픈씨 클론 코딩 프로젝트 

목표 : 오픈씨 클론코딩과 협업

기간 : 약 5일
팀원 : 총 3명 

내가 맡은 역할 : NFT  Minting, Sale 스마트 컨트랙트 개발(개발수준이 아닌 공부...) / 프로젝트 기록

느낀 점 : 
코드스테이츠에서 프로젝트 1을 진행했었고 프로젝트1은 OpenSea 클론코딩이었다.
짧은 기간동안 저 무지막지한? OpenSea를 클론코딩은 하기 힘들것이라고 판단했고 최우선적으로 소통하고 협업하는 걸 공부해야겠다는 생각으로 임했다. 

전체적으로 나는 초반에 OpenSea의 핵심기능에 대해 큰 그림을 그리지 못해서 파편적인 지식으로 공부해서  시간을 많이 날려먹었고 스마트 컨트랙트도 어떻게 개발하고 테스트할지 감을 잡지 못했다. 그러던 중 다음 강의를 보고 NFT 마켓에 대한 개념을 이해했다.  개념을 명확히 모르고 진행한 점, 기획에 대해 생각을 깊이하지 못한 점, git을 생각보다 활용하지 못했던 점이 아쉬웠던 점이다. 그리고 나 스스로에 대해 너무 게으르다고 생각된 것이 OpenSea 스마트 컨트랙트는 내가 만든 2개의 컨트랙트 외에 컨트랙트를 관리하고 배포하는 기능이 더 많음에도 그것을 구현하고자 노력을 많이 하지 않았다는 것이 내가 게으르다고 생각한 이유다.
무언가를 알고 판단하는 것과 무언가를 알기도 전에 편견을 가지고 시도조차 하지 않는 것은 다르다. 
그리고 시행착오도 꽤 많이 했다.  링크 다음 글은 시행착오에 대해 적었다. 

링크

 

[무료] [D.P.(DappProject)] 디앱 프로젝트(NFT 생성, NFT 구매 및 판매) - 인프런 | 강의

리액트, 솔리디티(ERC721)를 활용하여 기본적인 NFT 생성, NFT 구매 및 판매를 배우게 됩니다. 스마트 컨트랙트 작성과 리액트를 활용하여 작성한 스마트 컨트랙트를 웹에 연동하는 법을 배우게 됩

www.inflearn.com

 

그리고 다음 OpenSea의 URL과 EIP문서들을 통해 NFT 마켓플레이스에 사용할 스마트컨트랙트와 개념들을  배울 수 있었다. 
https://github.com/ProjectOpenSea/opensea-creatures 

 

GitHub - ProjectOpenSea/opensea-creatures: Example non-fungible collectible, to demonstrate OpenSea integration

Example non-fungible collectible, to demonstrate OpenSea integration - GitHub - ProjectOpenSea/opensea-creatures: Example non-fungible collectible, to demonstrate OpenSea integration

github.com

 

https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md 

 

GitHub - ethereum/EIPs: The Ethereum Improvement Proposal repository

The Ethereum Improvement Proposal repository. Contribute to ethereum/EIPs development by creating an account on GitHub.

github.com

https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1155.md#erc-1155-metadata-uri-json-schema

 

GitHub - ethereum/EIPs: The Ethereum Improvement Proposal repository

The Ethereum Improvement Proposal repository. Contribute to ethereum/EIPs development by creating an account on GitHub.

github.com

 

 

내가 했던 시행착오들:


1. 백엔드에 대한 고민 :
짧은 기간동안 백엔드를 다 만들 수 있을까? 라는 질문과 함께 어중간하게 Node JS를 이용해서 api를 만들려고 했으나 시간도 짧았고 스마트 컨트랙트도 개념을 이해못한 상황에서 이 부분에서 너무 시간을 많이 낭비했다. 여기서 집중할 것은 집중하고 버릴것은 빠르게 버린 뒤 프로젝트를 진행하면서 뒤를 보면 안됐는데 여러번 백엔드에 대한 미련 때문에 뒤를 봤던 것 같다. 



2. 저장소에 대한 고민 

IPFS와 S3, 오픈소스 오브젝트 스토리지 Minio?에 대한 고민이 컸다. 처음에는 S3를 쓰려고 했으나 어쨌든 아마존 서비스이고 중앙화된 서비스이며 블록체인 프로젝트를 하는 순간에는 IPFS를 쓰고 싶은 마음이 강해졌다. 그래서 IPFS를 쓰려고 여러가지 서비스를 찾아봤다. 이후 찾은 것은 피나타? IPFS  서비스였고 무료로 사용하려고 했으나 너무 느리고, 비용도 비싸서 못쓰겠다는 생각이 들었다. 이후 그냥 오라클 클라우드에서 제공하는 클라우드 서버로 MInio 오브젝트 스토리지를 사용해볼까 했으나 구축하면서 시간과 리소스가 너무 많이 투입됐다. 그래서 안되겠다고 판단하고 구글링을 하던 중 "Third Web Storage"서비스에 대해 알게됐다. 피나타 같이 NFT를 IPFS 서비스에 올리는 서비스인데 빠르고 거기에다 무료였다. 그리고 API 사용법도 너무 쉬워서 이용했는데 

이 Dapp 프로젝트에서는 안성맞춤이었다. 그래서 이 서비스를 활용해서 NFT를 업로드했다. 테스트는 curl로 했으며 예제는 다음과 같다. 


Third Web Storage (React 버전)

설치

npm install @thirdweb-dev/react ethers@5

 

코드

import { useStorageUpload } from "@thirdweb-dev/react";

export default function Component() {
  const { mutateAsync: upload } = useStorageUpload();

  async function uploadData() {
    // Get any data that you want to upload
    const dataToUpload = [...];

    // And upload the data with the upload function
    const uris = await upload({ data: dataToUpload });
  }

  ...
}

 

Third Web Storage (Node JS 버전, 내가 만들어서 사용했던 간략한 코드 )


설치

npm install express body-parser cors express-fileupload morgan lodash multer --save


코드

//라이브러리 설정 
const { ThirdwebStorage } = require("@thirdweb-dev/storage");
const fs = require("fs");
const express = require('express');
const fileUpload = require('express-fileupload');
const app = express();

//port 사용 
const port = 8999;
//웹3 컨트랙트 사용법 
const Web3 = require('web3');

app.use(fileUpload());

app.post('/upload', (req, res) => {
    if (!req.files || Object.keys(req.files).length === 0) {
      return res.status(400).send('No files were uploaded.');
    }
    //console.log("req.files",req)
    console.log("--- ,req.files.files.file1[name])",req.files["file1"]['name'])
    console.log("--- ,req.files.files.file1[name])",req.files["file1"])

    //console.log("** req.files.files ",req.files.files[0])
const storage = new ThirdwebStorage();
//console.log(req.files)
//let uploadFile = `/upload/${req.files.filename}` 

//third web storage를 통한 ipfs 업로드 후 url 리턴
(async () => {
 const upload = await storage.upload(fs.readFileSync(req.files["file1"]['name']));
  console.log(`Gateway URL - ${storage.resolveScheme(upload)}`);
  res.send(`${storage.resolveScheme(upload)}`);
})();



});


app.listen(3002, () => {
    console.log('Server listening on port 3002');
  });


Third Web Storage에서 제공하는 API 문서 

https://portal.thirdweb.com/storage/upload/ipfs

 

Upload to IPFS | thirdweb developer portal

You can upload arbitrary files and JSON data to IPFS using from both frontend and backend applications using our SDKs, or you can easily upload from your machine using our CLI.

portal.thirdweb.com

 

3. ERC721 컨트랙트의 표준함수들을 꼭 써야하는가? 
ERC721.sol 프로젝트 중 제공받은 ERC721 컨트랙트는 잘 동작하지 않았고 그래서 그냥 표준 ERC721 컨트랙트 코드를 사용했다. 처음에는 여러 함수들 중 필요없다고 생각한 것을 전부 제거했으나 알고보니 ERC721 표준 컨트랙트에서 꼭 써야하는 컨트랙트가 있었다. 그래서 다음 MintToken에서 필요한 함수들까지 전부 쓰면서 컨트랙트를 배포했다. 

 

4. 개발환경 설정의 어려움 
가나슈를 이용해서는 로컬에서 테스트를 하고 또 프론트와 협업할 때는 고얼리 같은 네트워크를 사용해야겠다고 생각해서 개발을 진행했으나 고얼리로 꼭 보고싶거나 테스트하고 싶은 함수가 있어서 테스트할 때 고얼리를 많이 사용했다. 아무래도  고얼리는 하루동안  테스트 ETH를 받을 수 있는 한계가 있다보니 금방 ETH를 사용했다. 그래서 뭔가 아쉬웠다. 왜 테스트넷에서 개발을 하는데 ETH를 이렇게 조금밖에 못받지? 내가 그냥 테스트 네트워크를 구축할 수 없나? 라는 질문도 가졌으나 실제로 실행해본 것은 없었다. 

 

 

다음은 내가 위의 강의와 ERC721의 표준 컨트랙트를 보고 응용을 조금만 해서 만든 컨트랙트들이다. 
자기반성용으로 업로드해봤다. 

 

MintToken.sol 

// SPDX-License-Identifier: MIT 

pragma solidity ^0.8.0; 

import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

//다음과 같이 짧은 코드로 mint 함수를 만들수 있다. 
contract MintToken is ERC721Enumerable, ERC721URIStorage,Ownable {
    // constructor : 스마트컨트랙트가 빌드될 때 한번 실행함 
    // ERC721(name, symbol)
    constructor() ERC721("sky","SKY"){}

    //앞 uint256은 animalTokenId 뒤 uint256은 animalTypes 
    mapping(uint256 => uint256) public nftTypes; 


  //오픈 제플린 라이브러리를 쓰고 상속할 경우 필수로 써야함
  //토큰 전송전 체크 
    function _beforeTokenTransfer(
        address from,
        address to,
        uint256 nftTokenId,
        uint256 batchSize
    ) internal  override(ERC721, ERC721Enumerable)
    {
        super._beforeTokenTransfer(from, to, nftTokenId,batchSize);
    }

  //오픈 제플린 라이브러리를 쓰고 상속할 경우 필수로 써야함
  //소각하는 함수 
        function _burn(
        uint256 nftTokenId
    ) internal
      override(ERC721, ERC721URIStorage) {
        super._burn(nftTokenId);
    }

  //오픈 제플린 라이브러리를 쓰고 상속할 경우 필수로 써야함
  //인터페이스 지원하는 함수 
   function supportsInterface(bytes4 interfaceId)
        public
        view
        override(ERC721, ERC721Enumerable)
        returns (bool)
    {
        return super.supportsInterface(interfaceId);
    }
    

    function tokenURI(
    uint256 nftTokenId
    ) public view
      override(ERC721, ERC721URIStorage) returns (string memory) {
        return super.tokenURI(nftTokenId);
    }
    
    function mintToken(address recipient, string memory tokenURI) public onlyOwner returns (uint256) {
        //tokenid가 유일해야 NFT라고 할 수 있다. 
        uint256 nftTokenId = totalSupply() + 1;

        //솔리디티에서 랜덤은 다음과같이 만든다. 
        uint256 nftType = uint256(keccak256(abi.encodePacked(block.timestamp, msg.sender, nftTokenId))) % 5 +1; 

        //배열에 값을 선언한다. 
        nftTypes[nftTokenId] = nftType; 

        //erc721에서 제공해주는 함수 _mint
        //msg.sender는 컨트랙트를 발행하는 사람, owner / animalTokenId는 NFT임을 증명하는 고유의 넘버 
        _mint(recipient, nftTokenId);
        //tokenURI : nft 이미지 주소 받는 곳, animalTokenId : NFT 아이디 
        _setTokenURI(nftTokenId, tokenURI);

        return nftTokenId;
    }
}

 

SaleToken.sol

// SPDX-License-Identifier: MIT 

//버전 명시 
pragma solidity ^0.8.0; 

//mint 함수 컨트랙트 추가 
import "./MintToken.sol";

//판매 컨트랙트 설정 
//민트 컨트랙트가 있어야 하므로 실행과정은 다음과 같다.
// MintToken을 deploy
// 이후 SaleToken 배포
// mintToken 함수로 NFT를 민트한다.  
// MintToken의 setApprovedForAll 함수에서 operator로 판매 컨트랙트주소를 넣고 bool값에 true로 권한을 허용해줘야함 
// 이후 isApprovedForAll을 실행해준다. owner 계정과 판매컨트랙트 계정을 입력해준다. 
// 이후 SaleToken의 setForSaleToken함수를 호출한다.  가격과 토큰 아이디를 설정한다. 



contract SaleToken {
    //MintToken 어드레스를 담을 주소를 선언 
    MintToken public mintTokenAddress;

    //생성자, 컨트랙트 빌드시 한번 실행됨 
    constructor (address _mintTokenAddress){
        mintTokenAddress = MintToken(_mintTokenAddress);
    }   

    //nftTokenId가 입력이면 출력은 nftTokenPrices로 설정함 
    //왜? 속성 체크때문에 설정했다.
    mapping(uint256 => uint256 ) public nftTokenPrices; 

    //프론트에서 이 배열을 가지고 어떤 토큰이 판매중인지 확인하기 위한 배열 
    uint256[] public onSaleTokenArray; 


    //함수 인자에 대한 설명 
    //무엇을 판매할지 >> nftTokenId(왜? 유일한 값이니까) , 얼마에 팔지 >> _price
    //함수범위는 public  
    function setForSaletoken(uint256 _nftTokenId, uint256 _price ) public {
        // address 변수 nftTokenOwner = constructor에서 생성한 mintTokenAddress의 속성 ownerOf(주인이 누군지 출력해줌)
        address nftTokenOwner = mintTokenAddress.ownerOf(_nftTokenId);

        //주인이 맞는지 확인 require로 조건 체크 1 / 아닐 경우 다음 에러 출력 
        require(nftTokenOwner == msg.sender, "Caller is not  token owner! ");

        //가격이 0초과여야 한다. 아닐 경우 다음 에러 출력  
        require(_price > 0, "Price is zero or lower.");

        //nftTokenPrices의 가격이 0인지 체크, 0이 아니라는 것은 이미 판매중이라는 뜻 따라서 0이 아닐 경우 다음과 같은 에러가 출력된다. 
        require(nftTokenPrices[_nftTokenId] == 0, "This  token is already on sale");

        //isApprovedForAll에는 2개의 인자가 있다. 첫 번째는 컨트랙트의 Owner, 2번째 address[this]에서 this는 이 컨트랙트(sales contract)를 말한다. 
        //이 주인이 이 판매계약서, 즉 스마트 컨트랙트의 판매권한을 넘겼는지 체크하는 구문이라고 보면된다. 왜냐하면 이 스마트컨트랙트가 1개의 파일인데 
        // 이상한 스마트컨트랙트로 코인을 보내면 코인이 묶여서 영원히 찾을 수 없다. 그래서 이 함수가 ERC721에서 만들어졌다. 
        //true일 때만 판매 등록 가능 false는 판매등록 안됨 
        require(mintTokenAddress.isApprovedForAll(nftTokenOwner, address(this)), " token owner did not approve token." );

        //nftTokenId에 해당하는 값을 넣어준다.
        nftTokenPrices[_nftTokenId] = _price;

        //판매 중인 _nftTokenId, 즉 NFT를 배열에 추가해준다. 
        onSaleTokenArray.push(_nftTokenId);

    }

    //구매 컨트랙트 
    function purchaseToken(uint256 _nftTokenId) public payable{
        //판매 가격 변수 설정 
        uint256 price = nftTokenPrices[_nftTokenId];
        
        //mintTokenAddress의 주소의 owner 변수 설정 
        address nftTokenOwner = mintTokenAddress.ownerOf(_nftTokenId);

        //판매금액이 0보다 큰지 체크 
        require(price > 0, " token not sale");

        //msg.value는 이 함수를 실행할 때 보내는 토큰 양
        require(price <= msg.value, "Caller sent lower than prie" );

        //판매할 때 판매자가 owner인것과는 달리 구매자는 owner가 아니여야 구매가 되므로 설정함
        //즉 token owner와 구매자는 같지 않다는 것을 체크하기 위한 구문 
        require(nftTokenOwner != msg.sender, "Caller is  token owner");

        //구매로직 
        //다음 구문 때문에 함수에서 payable이 필요했다. 
        //이 함수를 실행한 msg.sender에게 msg.value(nft의 가격)만큼의 토큰 양이 nftTokenOwner에게 간다. 
        //즉 구매처리되서 판매가격은 주인한테 전송한다는 뜻이다. 
        payable(nftTokenOwner).transfer(msg.value);

        //인자 3개, 첫 번재 보내는 사람, 받는 사람, 애니멀 토큰아이디(NFT 고유아이디)
        //다음은 토큰을 전송하는 함수로 NFT표준에 등록되어있는 함수 
        mintTokenAddress.safeTransferFrom(nftTokenOwner, msg.sender, _nftTokenId);        


        //매핑에서 제거된 것은 구매 완료처리된 속성
        nftTokenPrices[_nftTokenId] = 0;

        //판매중인 목록에서 제거, 배열에서 제거 
        //onSaleTokenArray 판매중인 NFT 배열 크기만큼 순회 
        for(uint256 i=0; i < onSaleTokenArray.length; i++){
            //위 코드에서 매핑에서 제거한 값이 0이되었으므로 구매로직이 끝난 NFT토큰을 조건문으로 찾는다. 
            if(nftTokenPrices[onSaleTokenArray[i]] == 0 ){
                //onSaleTokenArray[i] 즉 구매로직이 끝난 배열값에 마지막 배열값을 대입하면 
                //구매로직이 끝난 배열값은 사라진다. 
                
                //예를 들면 onSaleTokenArray의 길이가 총 5이고 마지막 값이 3일 때 (onSaleTokenArray[4]==3) 
                //onSaleTokenArray[2] == 0 일 때 3번째 인덱스의 값이 매핑에서 제거되고 구매도 끝났다면 
                // onSaleTokenArray[2] == 3(마지막 배열 값을 대입)한 후 배열에서 마지막 값을 pop으로 빼주면
                //구매로직이 끝난 배열은 사라졌고 기존 마지막 배열값도 사라졌으므로 판매중인 NFT 목록에서 완벽하게 제거됐다. 
                onSaleTokenArray[i] = onSaleTokenArray[onSaleTokenArray.length-1];
                onSaleTokenArray.pop(); 
            }

        }
    }
    //view로 함수를 만든 이유는 프론트에서 for문으로 배열값을 찾아서 리턴하면 느리므로
    //스마트 컨트랙트에서 데이터를 리턴해주는 조회용 함수다. 
    //판매중인 토큰의 길이를 리턴해주므로 메인페이지나 전체 판매중인 토큰 개수를 체크할 때 유용하다. 
    function getOnSaleTokenArrayLength() view public returns (uint256){
        return onSaleTokenArray.length;
    }

    //nft 토큰의 가격을 반환해주는 함수 
    function getTokenPrice(uint256 _nftTokenId) view public returns (uint256){
        return nftTokenPrices[_nftTokenId];
    }
}

 

 

 

관련글 더보기