[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
댓글남기기