[Web3/Dapps Project] 간단한 NFT Application 제작기 🎨🖼

업데이트:

NFT Application 🖼:: 아이돌 포토카드 거래소

최근에 NFT와 블록체인에 관심이 생겨 OpenSea 마켓도 열어보고, 여러 서적도 읽어보면서 공부를 해보고 있는데요 :)

오늘은 간단하게 제작해본 NFT 어플리케이션을 소개해드리고자 합니다! (튜토리얼보다는 코드 저장용 포스팅에 가깝네요😂) 아직 잘 몰라 틀린 부분이 있을 수도 있으니 오류가 있으면 댓글 부탁드립니다 😊

이더리움과 솔리디티

이더리움과 디앱(Dapps)

이더리움은 자체 블록체인을 기반으로 다양한 탈중앙화 된 애플리케이션들이 작동할 수 있도록 고안된 하나의 플랫폼 네트워크이다. 디앱은 이러한 이더리움 플랫폼 상에서 스마트 계약을 이용하여 쉽고 빠르게 토큰을 발행할 수 있다. 이더리움 블록체인에서는 이더(ETH)가 사용되고, 이더리움 블록체인 상의 디앱은 또 다른 다양한 분야에 적용될 수 있는 각각의 솔루션으로 그에 맞는 토큰을 발행하는 것이다. 이 때 발행된 토큰은 독자적인 토큰인 듯 하지만 실제로는 이더리움 생태계에서 호환 및 사용 가능하다.

예를 들어, 안드로이드 및 iOS가 하나의 플랫폼 역할을 하고 그 위에 존재하는 수많은 앱들이 있다. 단순 비교해보면, 안드로이드 및 iOS가 이더리움 플랫폼 역할을 하고 그 위에 존재하는 앱이 디앱과 같은 것이다. (출처: 해시넷)

솔리디티란?

솔리디티(Solidity)는 이더리움 등 블록체인 플랫폼에서 스마트 계약 작성과 구현에 사용되는 계약 지향 프로그래밍 언어이다. 솔리디티는 이더리움 핵심 기여자들에 의해 이더리움과 같은 블록체인 플랫폼상에 스마트 계약을 작성할 수 있도록 개발되었다. 개발자는 솔리디티를 통해서 스스로 실행되는 비즈니스 로직을 스마트 계약에 담아서 구현할 수 있다.[1] 솔리디티를 통해 다양한 앱을 구현할 수 있지만 블록체인 특성상 스마트 계약이 블록체인에 한번 올라가면 수정할 수 없으며 누구나 확인할 수 있기 때문에 신중하게 작성을 해야 한다.[2] (출처: 해시넷)

SR (Software Requirements)

🧑‍💻 기술 스택

  • Node.js
  • FrontEnd: React (using vite)
  • BackEnd: Solidity (using hardhat)

🧑‍💻 주요기능

  • 메타마스크 지갑연결
  • 분산화된 저장 시스템(IPFS)에 원본 데이터셋 업로드 & NFT 토큰 발행
  • ERC-721 토큰을 사용하여 디지털 원본 이미지에 대한 NFT 토큰 발행
    • OpenZepplin 참고
  • NFT 작품 Minting & Transaction

IPFS

IPFS는 분산된 P2P파일 시스템에 안전하게 NFT를 저장할 수 있기 때문에, 일반적인 URL과 같은 링크 끊김, 404오류와 같은 문제를 해결할 수 있다.

본 프로젝트에서는 IPFS를 이용하여 NFT거래에 사용된 디지털 원본 포토카드를 분산형 저장매체에 저장하였다.

IPFS란? (아이피에프에스)는 “InterPlanetary File System”의 약자로서, 분산형 파일 시스템에 데이터를 저장하고 인터넷으로 공유하기 위한 프로토콜이다. 냅스터토렌트(Torrent) 등 P2P 방식으로 대용량 파일과 데이터를 공유하기 위해 사용한다. 기존의 HTTP 방식은 데이터가 위치한 곳의 주소를 찾아가서 원하는 콘텐츠를 한꺼번에 가져오는 방식이었지만, IPFS는 데이터의 내용을 변환한 해시값을 이용하여 전 세계 여러 컴퓨터에 분산 저장되어 있는 콘텐츠를 찾아서 데이터를 조각조각으로 잘게 나눠서 빠른 속도로 가져온 후 하나로 합쳐서 보여주는 방식으로 작동한다. 해시 테이블은 정보를 키와 값의 쌍(key/value pairs)으로 저장하는데, 전 세계 수많은 분산화된 노드 들이 해당 정보를 저장하기 때문에 사용자는 IPFS를 사용함으로써 기존 HTTP 방식에 비해 훨씬 빠른 속도로 데이터를 저장하고 가져올 수 있다. (출처: 해시넷)

Install by command line

  • 다음 영상 참고
  • command line을 이용해서도 ipfs를 설치할 수 있다.

      # m1이라면 다음 command를 이용
      curl -O https://dist.ipfs.io/go-ipfs/v0.12.2/go-ipfs_v0.12.2_darwin-amd64.tar.gz
        
      # 압축해제
      tar -xvzf go-ipfs_v0.12.2_darwin-amd64.tar.gz
        
      # 설치
      cd go-ipfs
      bash install.sh
        
      > Moved ./ipfs to /usr/local/bin
        
      # 버전 확인
      ipfs --version
        
      > ipfs version 0.12.0
    
  • ipfs 서버 열기

       ipfs init                                                                                   16:37:39
      generating ED25519 keypair...done
      peer identity: 12D3KooWCixEwUyDUbJbcZN7967PB2G14ipu8HZTQEbhYnb1B1xp
      initializing IPFS node at /Users/user/.ipfs
      to get started, enter:
        
              ipfs cat /ipfs/QmQPeNsJPyVWPFDVHb77w8G42Fvo15z4bG2X8D2GhfbSXc/readme
        
       ipfs daemon
      API server listening on /ip4/127.0.0.1/tcp/5001
      WebUI: http://127.0.0.1:5001/webui
      Gateway (readonly) server listening on /ip4/127.0.0.1/tcp/8080
      Daemon is ready
    
  • 다음과 같이 웹페이지가 호스팅됨

  • 파일 업로드

    • IPFS 게이트웨이로 보아도 이미지가 잘 업로드 되었음을 확인할 수 있음

💻 Web3

Frontend

프론트앤드 지원 툴인 vite를 이용하여 프론트앤드를 생성

  • 동적으로 UI를 만들 수 있는 React 웹 프레임워크를 사용하여 app을 생성하였음
 npm init vite myapp                                                                                                       16:47:17

Need to install the following packages:
  create-vite
Ok to proceed? (y) y
 Select a framework:  react
 Select a variant:  react

Scaffolding project in /Users/user/Code/blockchain/myapp...

Done. Now run:

  cd myapp
  npm install
  npm run dev

backend

  • hardhat을 이용하여 dapp 개발
    • hardhat이란?
      • ethereum 개발을 할 때 compile, deploy, test를 모두 진행할 수 있는 개발 프레임워크
      • 트러플과 유사한 이더리움 기반 스마트 컨트랙트 개발 도구로, 자바스크립트 기반으로 자유도가 높고 유연한 개발 환경을 제공하며, 특히 스마트 컨트랙트에서 console.log를 사용하여 값을 출력할 수 있는 기능도 제공한다.
      • 요즘은 truffle보다 hardhat이 대세라고 하더라구요ㅎㅎ
    • install
        npx hardhat
        npm install --save-dev @nomiclabs/hardhat-waffle ethereum-waffle chai @nomiclabs/hardhat-ethers ethers @openzeppelin/contracts
      
    • hardhat은 node에서 동작하기 때문에 react와 연동이 쉽다.
      • hardhat.config.js에서 artifact의 컴파일 경로를 업데이트해서 react랑 연동하였다.

          module.exports = {
            solidity: "0.8.4",
            paths: {
              artifacts: './src/artifacts'
            }
          };
        

Token

  • ERC-20
    • Fungible Tokens
    • ERC-20은 Ethereum Request for Comments 20의 약자로 이더리움 네트워크 상에서 유통할 수 있는 토큰의 호환성을 보장하기 위한 표준 사양이다. ERC-20 토큰은 스마트 계약의 속성을 지원해야만 한다. 스마트 계약은 온라인 환경에서 암호화폐 교환 시, 일정 행동이 불가역적으로 전개되는 기능을 통해, 중앙관리가 배제된 서비스를 구현할 수 있는 것이 바로 스마트 계약의 강점이며, 이더리움 블록체인에서 구현이 용이하다. 따라서, 디앱은 이더리움 블록체인 플랫폼을 활용해 자신의 비즈니스를 구현하고, 자금모집 및 거래체계, 플랫폼 사용료를 이더리움으로 지불하는 체계를 가지고 토큰을 발행할 필요가 크며, 실제로 이더리움 기반으로 토큰 발행이 많다. (출처: 해시넷)
  • ERC-721

    우리가 사용할 NFT토큰!

    • Non Fungible Tokens (NFT)
    • EIP-721 이라고해서 이더리움 개선 제안의 721번째 토론에서 채택된 각각 구분 가능한 토큰.
    • ERC-721은 증서라고 알려진 NFT의 표준안이다. NFT는 대체불가토큰(Non Fungible Token)의 약자로 대체 불가능한 토큰이라는 의미이다. 따라서 ERC-721로 발행되는 토큰은 대체 불가능하여 모두 제 각각의 가치(Value)를 갖고 있다.
    • ERC-721은 토큰 그 자체보다는 게임에 주로 쓰이는데, 대표적인 예로는 크립토키티(CryptoKitties)가 있다. 크립토키티의 고양이들은 제 각각 다른 생김새를 가지고 있다. 따라서 사용자가 보유하고 있는 고양이는 전 세계에서 단 하나밖에 없는 유일한 고양이가 된다.

OpenZepplin

  • 솔리디티 기반의 스마트 컨트랙트를 개발하는데 도움을 주는 프레임워크
  • install

      npm install @openzeppelin/contracts
    
  • OpenZepplin 사이트에 들어가면.. 다음과 같이 NFT 토큰을 발행해주는 소스 코드를 제공해준다.

    본 프로젝트에서는 오픈 제플린에 나와있는 ERC721사용법에 따라 minting을 하고, 돈을 내고.. 하는 코드를 작성하였다.

    • Code

        // SPDX-License-Identifier: MIT
        pragma solidity ^0.8.4;
              
        import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
        import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
        import "@openzeppelin/contracts/access/Ownable.sol";
        import "@openzeppelin/contracts/utils/Counters.sol";
              
        contract FiredGuys is ERC721, ERC721URIStorage, Ownable {
            using Counters for Counters.Counter;
              
            Counters.Counter private _tokenIdCounter;
              
            mapping(string => uint8) existingURIs;
              
            constructor() ERC721("MyToken", "MTK") {}
              
            function _baseURI() internal pure override returns (string memory) {
                return "ipfs://";
            }
              
            // Only Owner
            function safeMint(address to, string memory uri) public onlyOwner {
                uint256 tokenId = _tokenIdCounter.current();
                _tokenIdCounter.increment();
                _safeMint(to, tokenId);
                _setTokenURI(tokenId, uri);
            }
              
            // The following functions are overrides required by Solidity.
              
            function _burn(uint256 tokenId) internal override(ERC721, ERC721URIStorage) {
                super._burn(tokenId);
            }
              
            function tokenURI(uint256 tokenId)
                public
                view
                override(ERC721, ERC721URIStorage)
                returns (string memory)
            {
                return super.tokenURI(tokenId);
            }
              
            function isContentOwned(string memory uri) public view returns (bool) {
                return existingURIs[uri] == 1;
            }
              
            function payToMint(
                address recipient,
                string memory metadataURI
            ) public payable returns (uint256) {
                require(existingURIs[metadataURI] != 1, 'NFT already minted!');
                require (msg.value >= 0.05 ether, 'Need to pay up!');
              
                uint256 newItemId = _tokenIdCounter.current();
                _tokenIdCounter.increment();
                existingURIs[metadataURI] = 1;
              
                _mint(recipient, newItemId);
                _setTokenURI(newItemId, metadataURI);
              
                return newItemId;
            }
              
            function count() public view returns (uint256){
                return _tokenIdCounter.current();
            }
        }
      

Transaction & Mint

발행한 토큰을 불러오고, 누군가 돈을 내면 NFT 작품을 minting을 하는 코드

  • test/sample-test.js

      const { expect } = require("chai");
      const { ethers } = require("hardhat");
        
      describe("myNFT", function () {
        it("Should mint and transfer an NFT to someone", async function () {
          const FiredGuys = await ethers.getContractFactory("FiredGuys");
          const firedguys = await FiredGuys.deploy();
          
          await firedguys.deployed();
        
          // recipient: `npx hardhat node`에서 얻은 public key 값
          const recipient = '0x15d34aaf54267db7d7c367839aaf71a00a2c6a65';
          const metadataURI = 'cid/test.png';
        
          let balance = await firedguys.balanceOf(recipient);
          expect(balance).to.equal(0);
        
          const newlyMintedToken = await firedguys.payToMint(recipient, metadataURI, {value: ethers.utils.parseEther('0.05')});
        
          // wait until the transaction is minted
          await newlyMintedToken.wait()
            
          balance = await firedguys.balanceOf(recipient);
          expect(balance).to.equal(1);
            
          expect(await firedguys.isContentOwned(metadataURI)).to.equal(true);
        
        });
      });
    
      // test
      npx hardhat test
    
  • scripts/sample-script.js

      const hre = require("hardhat");
        
      async function main() {
        
        const FiredGuys = await hre.ethers.getContractFactory("FiredGuys");
        const firedGuys = await FiredGuys.deploy();
        
        await firedGuys.deployed();
        
        console.log("firedguys deployed to:", firedGuys.address);
      }
        
      main()
        .then(() => process.exit(0))
        .catch((error) => {
          console.error(error);
          process.exit(1);
        });
    
  • src/components
    • Home.jsx

        import WalletBalance from './WalletBalance';
        import { useEffect, useState } from 'react';
              
        import { ethers } from 'ethers';
        import FiredGuys from '../artifacts/contracts/MyNFT.sol/FiredGuys.json';
              
        // npx hardhat run scripts/sample-script.js --network localhost   
        const contractAddress = '0x5FbDB2315678afecb367f032d93F642f64180aa3';
        const provider = new ethers.providers.Web3Provider(window.ethereum);
              
        // get the end user
        const signer = provider.getSigner();
        // get the smart contract
        const contract = new ethers.Contract(contractAddress, FiredGuys.abi, signer);
              
        function Home() {
              
          const [totalMinted, setTotalMinted] = useState(0);
          useEffect(() => {
            getCount();
          }, []);
              
          const getCount = async () => {
            const count = await contract.count();
            console.log(parseInt(count));
            setTotalMinted(parseInt(count));
          };
              
          return (
            <div>
                <WalletBalance />
                <h1> 🖼 Idol PhotoCard - NFT Market </h1>
                  {Array(totalMinted + 1)
                  .fill(0)
                  .map((_, i) => (
                      <div key={i}>
                      <NFTImage tokenId={i} getCount={getCount} />
                      </div>
                  ))}
            </div>
          );
        }
              
        function NFTImage({ tokenId, getCount }) {
          const contentId = 'QmSRccBaE2Xb92D6umiZbwbABCcGB62kDE19ZoudwC7uSR';
          const metadataURI = `${contentId}/${tokenId}.json`;
          // const imageURI = `${contentId}/${tokenId}.png`;
          const imageURI = `../../out/${tokenId}.png`;
                
          const [isMinted, setIsMinted] = useState(false);
                
          useEffect(() => {
            getMintedStatus();
          }, [isMinted]);
              
          const getMintedStatus = async () => {
            const result = await contract.isContentOwned(metadataURI);
            console.log(result)
            setIsMinted(result);
          };
              
          const mintToken = async () => {
            const connection = contract.connect(signer);
            const addr = connection.address;
            const result = await contract.payToMint(addr, metadataURI, {
              value: ethers.utils.parseEther('0.05'),
            });
              
            await result.wait();
            getMintedStatus();
          };
              
          async function getURI() {
            const uri = await contract.tokenURI(tokenId);
            alert(uri);
          }
          return (
            <div>
              <img src={ imageURI } width={256} ></img>
                <h5>ID #{tokenId}</h5>
                {!isMinted ? (
                  <button onClick={mintToken}>
                    🖼 포토카드 구매하기
                  </button>
                ) : (
                  <button onClick={getURI}>
                    구매가 완료되었습니다! URI을 확인하세요 :)
                  </button>
                )}
            </div>
          );
        }
              
        export default Home;
      
    • Install.jsx

        const Install = () => {
            return (
              <div>
                <h3>Follow the link to install 👇🏼</h3>
                <a href="https://metamask.io/download.html">Meta Mask</a>
              </div>
            );
          };
                
        export default Install;
      
    • WalletBalance.jsx

        import { useState } from 'react';
        import { ethers } from 'ethers';
              
        function WalletBalance() {
              
            const [balance, setBalance] = useState();
                  
            const getBalance = async () => {
                const [account] = await window.ethereum.request({ method: 'eth_requestAccounts' });
                const provider = new ethers.providers.Web3Provider(window.ethereum);
                const balance = await provider.getBalance(account);
                setBalance(ethers.utils.formatEther(balance));
            };
                
            return (
              <div>
                  <h5>내 자산: {balance}</h5>
                  <button onClick={() => getBalance()}> 💸💵 지갑에 있는 돈 확인하기 </button>
              </div>
            );
          };
                
          export default WalletBalance;
      
  • 웹 페이지 호스팅

      //background
      npx hardhat node
        
      npx hardhat run scripts/sample-script.js --network localhost
      // firedguys deployed to: 0x5FbDB2315678afecb367f032d93F642f64180aa3
    
      npm run dev
    

결과

메타마스크 지갑 연결

NFT 작품 Minting & Transaction


Reference

댓글남기기