codestates_project1_opensea clone coding

독수리박박·2023년 6월 29일
0

project

목록 보기
1/3

Project 1 회고 및 나의 개발 일지

인생 첫 프로젝트

사실 이 글이 내가 코딩을 시작이래로 작성하는 첫 블로그 글이다. 코드스테이츠 블록체인 9기를 수강한지 벌써 4개월이 지났지만 혼자 로컬 파일이나 수기로 정리 해놓았을 뿐 블로깅은 미루고 미루다 결국 첫 프로젝트가 끝나고 나서야 작성한다...

사실 이유는 아직 대학교 재학중이기도 하고 개발 기초지식이나 알고리즘 공부, 언어 공부만 하다가 실제로 그것들을 구현해 보는 시간이 이번 부트캠프가 처음이었기에 하루하루 진도를 따라가고 이해하는데 너무 바빳다.. 물론 여기서 얻은 것이 너무 많다 시작전과 비교하면 지금은 어느정도 서치를 통해서 무엇을 만들정도가 되었고 여기서 만난 사람들한테 정보를 줍줍하면서 점점 내가 성장한다는 것을 느꼈다...

사실 나는 프로젝트가 시작하기 전에 스터디나 페어활동을 통해서 이미 컨트랙트를 배포하고 어느정도 API 구현을 해봤기에 어떻게 진행해야 할지 대충 감을 잡았다. 물론 이것은 나만의 착각이었다! 우하하!!!!!

팀원분들 모두 다들 잘 몰라도 의지하며 열심히해서 잘 마무리가 된거 같다.
정말 감사하다.


프로젝트 시작

첫번째 프로젝트는 opensea 구현하기였다. NFT에 관심이 있었기에 나에게 좋은 주제라고 생각했다. 나는 벡엔드 및 스마트 컨트랙트 부분을 맡았고 시작하기에 앞서 대충 머리속의 그림을 그림을 그려봤다. 사실 다른 분들 모두 각자의 역할에 바빠 백엔드 부분을 혼자 만들어야하다 보니 많이 간소화 했다. 기회가 되면 정말 빠지는 거 없이 만들어 보고 싶다.

이것도 몇번의 수정을 거쳐서 만든 아키텍쳐이다.


동작원리

정말 잘 만든다면 로그인(메타마스크 연결)하면서 트랜잭션 전송에 필요한 정보??(트랜잭션 전송 시 결과에서 보이는 v,r,s)를 서버로 전해주고 JWT를 발급받아 트랜잭션 서명을 가능하게 하고 여러 동작들을 하게 하는 그런 원리로 가야한다.
(상준님이 잘 설명해주셨다ㅎㅎ 사실 아직 어려워요ㅠ.. 다음엔 꼭 구현해야지)

하지만 우리는 아직 그 정도 단계는 아니기에 그냥 컨트랙트 객체를 생성하고 컨트랙트 객체에 개인 지갑을 연결해 서버 지갑처럼 사용하면서 트랜잭션 생성 및 전송을 가능하게 했다.

그리고 opensea-testnet api는 sopolia를 지원안하기에 나중에 이걸 이용해서 발행한 토큰이나 메인 페이지에 채워넣을 토큰을 가져오려면 goerli-test-net을 이용해서 발행 했다. 메인넷이 토큰이 있어야 되거나 pow방식으로 테스트 토큰을 얻어야 하는데 상준님이 기부해줌!!아직 많이 남았습니다....

피나타에는 photo_url을 먼저 pinata 올리고 가져온 photo_hash값을 다시 metadata객체에 넣어준 다음 다시 pinata에 넣고 tokenURI를 반환 받았다.

어차피 모두 테스트용이 였고 지금은 모두 지웠다. 연속되어 있는 2개가 한쌍의 image, metadata이다. 사실 저 앞에 이름까지는 생각 안하고 막 올렸던거 같다.

그리고 mintNFT 함수를 실행시켜서 goerli-test-net에 올리고 opensea를 확인해 보니 귀여운 오리너구리가 잘 올라왔다. 중간에 몇 가지 과정을 생략했지만 잘 올라가는 것을 확인할 수 있었다.


스마트 컨트랙트

사실 유어클래스에서 ERC721, ERC1155 형식의 토큰을 모두 발행할 수 있는 솔리디티 파일을 제공해줬다. 그러나 모두가 처음인 이 순간에 굳이 저렇게 많은 기능이 있고 복잡한 컨트랙트를 썼다간 minte도 제대로 못해볼거 같았다. 그래서 최대한 간추려서 다시짰다.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;

import "../node_modules/@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "../node_modules/@openzeppelin/contracts/utils/Counters.sol";
import "../node_modules/@openzeppelin/contracts/access/Ownable.sol";
import "../node_modules/@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";

contract Opensea is ERC721URIStorage, Ownable {
    using Counters for Counters.Counter;
    Counters.Counter private _tokenIds;

    constructor() public ERC721("OpenSea", "Team3") {}

    function mintNFT(address recipient, string memory tokenURI)
        public 
        returns (uint256)
    {
        _tokenIds.increment();

        uint256 newItemId = _tokenIds.current();
        _mint(recipient, newItemId);
        _setTokenURI(newItemId, tokenURI);

        return newItemId;
    }
}

openzeppelin에서 필요한 것들을 모두 가져오고 일단 mint라도 시키자는 생각으로 mint, tokenURI정도만 반환할 수 있는 정도로 컨트랙트를 구성했다. 단순해 보여도 ERC-721을 import하고 있기 때문에 왠만한 기능은 모두 구현할 수 있다.


스마트 컨트랙트 배포 및 검증

Foundry를 사용해서 배포하기로 결정했다. 혼자 CLI 환경에서 배포 및 검증을 연습을 해볼 때 많이 써봐서 나에게는 hardhat 보다 훨신 편했다.

```
forge create --rpc-url <your_rpc_url> \
--constructor-args "ForgeUSD" "FUSD" 18 1000000000000000000000 \
--private-key <your_private_key> \
--etherscan-api-key <your_etherscan_api_key> \
--verify \
src/MyToken.sol:MyToken
```

foundry 설치 및 환경설정 후 터미널에서 해당 명령어와 각 부분에 맞는 인자들을 넣어주면 필요한 solidity파일들을 한번에 배포 및 검증까지 해주고 자동으로 로컬환경에 abi 코드들을 저장할 수 있다.
익숙해지니 Remix환경에서 하는거보다 어렵고 옵션을 넣기가 어렵지만 훨씬 편한거 같다.
더 많은 기능들은 아래 링크에서 참고하면 된다.
foundry.docs


기술스택

  • web3.js or ethers.js: 이더리움 기반의 dApp (탈중앙화 애플리케이션) 개발을 위한 자바스크립트 라이브러리, 컨트랙트 객체와 지갑과 연결해주기 위해서 ethers.js를 사용했다. 사실 web3.js로는 구현에 실패했고 사용하다 보니 ehters.js가 훨신 직관적이고 간편했다.

  • IPFS 저장소: 분산형 데이터베이스, 피나타(Pinata)를 선택했다. 사실 이론 수업을 들으며 혼자 test-net에 NFT를 발행해본적 있었는데 그냥 익숙해서 선택했다. 그리고 docs에 설명도 잘 나와있어서 금방 적응하고 사용할 수 있었다.
    Pinata.docs

  • Node.js: Chrome V8 JavaScript 엔진으로 구동되는 오픈 소스 서버 사이드 런타임 환경


코드

사실 이 mint.js 말고도 owner, search 등 다른 기능들도 서버 부분에서 처리할 수 있게 조금 짜놓았지만 시간이 부족해 클라이언트와 연결이 힘들거 같아서 클라이언트와 연결해 동작하는 것은 이 mint.js만 해놓았고 나머지 부분들은 서버와 동작하기 보다는 opensea-testnet api를 사용해 클라이언트에서 정보를 가져온 후 바로 뿌려줬다.

const express = require('express');
const router = express.Router();
const pinataSDK = require('@pinata/sdk');
const pinata = new pinataSDK(process.env.PINATA_API_KEY, process.env.PINATA_SECRET);
const { ethers } = require('ethers');
const abi = require('../out/Solidity.sol/Opensea.json').abi;


router.post('/', async (req, res) => {
  try {
    
    // Infura와 연결하기 위한 URL 설정.
    const url = "https://goerli.infura.io/v3/" + process.env.INFURA_API_KEY; //Infura API URL을 설정한다.
    const provider = new ethers.providers.JsonRpcProvider(url); // Ethereum 노드에 연결하기 위한 Provider를 생성한다.

    //원래는 지갑을 웹에 연결하면 여기서 v,r,s값을 만들어서 서버에 넘겨주면 해당 v,r,s값으로 트랜잭션 발행

    // Smart Contract와의 연결을 위한 설정.
    let contract = new ethers.Contract(process.env.SMARTCONTRACT_ADDRESS, abi, provider); // Smart Contract와 상호작용 하기 위한 인스턴스를 생성한다.
    const privateKey = process.env.PRIVATE_KEY;  //   {     프라이빗 키를 사용하여 
    const wallet = new ethers.Wallet(privateKey, provider);//    지갑을 생성한다.          } 
    contract = contract.connect(wallet);  // 지갑을 사용해 Smart Contract와 연결한다.

    // call(): 그냥 결과만 반환 , send(): 실제로 블록체인 상에 트랙잭션을 날림 = web3.js

    // 요청 Body에서 필요한 데이터들 추출하기.
    const { photoUrl, nftName, description, toAddress } = req.body;

    // 이미지 파일을 Pinata에 업로드 하는 코드.
    const photoResult = await pinata.pinJSONToIPFS({ image_url: photoUrl });
    console.log('Photo uploaded successfully. IPFS Hash:', photoResult.IpfsHash); 
  

    // NFT 메타 데이터 생성 및 Pinata에 업로드 하기
    const nftMetadata = {
      name: nftName,
      description: description,
      token_metadata: `ipfs://${photoResult.IpfsHash}`,
      image_url: photoUrl,
    };
  
    const metadataResult = await pinata.pinJSONToIPFS(nftMetadata); //pinata에 업로드. -> 완료시 아래 코드에서 반환된 IpfsHash 출력. 
    console.log('Metadata uploaded successfully. IPFS Hash:', metadataResult.IpfsHash);


    // 생성된 NFT의 CID와 token URI 설정 .to
    // const CID = metadataResult.IpfsHash;
    const tokenURI = `ipfs://${metadataResult.IpfsHash}`;
  
    // Smart Contract를 사용하여 NFT를 발행. (Mint)
    const mintResult = contract.mintNFT(toAddress, tokenURI) //toAddress-> NFT를 수령할 주소.  tokenURI-> 새로 생성된 NFT에 대한 메타데이터 URI
      .then((result) => {
        console.log(result);
        res.status(200).json({ tokenId: result, message:'OK' });
      });
  } catch (error) {
    console.error('Error adding to IPFS:', error);
    res.status(400).send(error);
  }
});

module.exports = router; 

느낀점

사실 첫 프로젝트이기도 하고 나도 아직 실력이 모자라기 때문에 완성을 못할까봐 조금 겁이 났다. 팀원분들도 아직 이런 프로젝트에 익숙하지 않아서 완성을 목표로 하고 여러가지를 건드리기 보다는 몇가지의 기능에 집중했던거 같다. 혼자 만들어 보고 테스트하며 여러가지 배운것도 많고 더 만들지 못하고 구현하지 못한 기능들에 대한 후회도 조금 남았다. 그래도 같이 고생한 팀원분들이 계셔서 완성할 수 있었던 것 같다. 이번에 다음부터는 이번에 느낀점을 토대로 더 계획적으로 움직이고 더 많은 것을 기록해두어야겠다. 그리고 블로깅도 이제는 미루지 말고 최대한 많이 쓰자. 게으른 놈아.

1개의 댓글

comment-user-thumbnail
2023년 7월 19일

박상현 멋지다.

답글 달기