작성중
본 글에서는 DApp 두 번째 프로젝트 NFT Marketplace, My Virtual Animal를 진행하며 관련 내용을 정리하겠습니다.
지난 DEX Superswap 프로젝트와 다르게, Next.js 프레임워크를 사용했습니다. 스마트 컨트랙트는 오픈제플린을 사용하면서도 백엔드 서버를 대체할 수 있도록 필요하다고 생각하는 기능들을 추가로 작성했습니다.
프로젝트 진행 중 주요 기능 개발 내용을 순서대로 나열했고, 작성한 코드를 중점으로 기능 설명을 추가했습니다. 따라서, 환경 설정과 관련된 기초 자료들은 해당 Velog에서 작성된 참고자료 링크로 대체했습니다.
Infura 회원가입 후 API Key, Goerli Testnet Endpoint 생성
참고자료: Goerli 이더리움 테스트넷 사용하기 with Infura
메타마스크 설치 후 계정 생성, Georli Testnet 설정, GoerliETH 발급 받기
참고자료: Goerli 이더리움 테스트넷 사용하기 with Infura
프로젝트에서 사용할 하나의 컨트랙트 MvaV1Market를 생성했다. 해당 컨트랙트는 ERC-721 토큰을 생성하고, 토큰에 URI 정보를 추가할 수 있다. 추가적으로, 클라이언트에서 필요한 NFT 토큰 정보를 관리할 수 있는 몇가지 상태 변수를 추가했다.
contract MvaV1Market is ERC721URIStorage {
uint256 private _nextTokenId;
mapping(uint256 => uint256) private _nftPrices;
mapping(uint256 => bool) private _isForSale;
mapping(address => uint256[]) private _ownedTokens;
event NFTPriceUpdated(uint256 indexed tokenId, uint256 price);
event NFTSaleStatusUpdated(uint256 indexed tokenId, bool isForSale);
event NFTSold(uint256 indexed tokenId, address indexed buyer, address indexed seller, uint256 price);
constructor() ERC721("MyVirtualAnimal", "MVA"){}
}
contract MvaV1Market is ERC721URIStorage {
function createNFT(address owner, string memory tokenURI)
public
returns (uint256)
{
uint256 tokenId = _nextTokenId++;
_mint(owner, tokenId);
_setTokenURI(tokenId, tokenURI);
_ownedTokens[owner].push(tokenId);
return tokenId;
}
}
contract MvaV1Market is ERC721URIStorage {
function setNFTPrice(uint256 tokenId, uint256 price) public {
require(ownerOf(tokenId) == msg.sender, "Only owner can set the price");
_nftPrices[tokenId] = price;
emit NFTPriceUpdated(tokenId, price);
}
function setNFTForSale(uint256 tokenId, bool isForSale) public {
require(ownerOf(tokenId) == msg.sender, "Only owner can set the sale status");
require(_nftPrices[tokenId] > 0, "NFT price should be greater than 0");
if (isForSale) approve(address(this), tokenId);
_isForSale[tokenId] = isForSale;
emit NFTSaleStatusUpdated(tokenId, isForSale);
}
}
contract MvaV1Market is ERC721URIStorage {
function buyNFT(uint256 tokenId) public payable {
require(_isForSale[tokenId], "NFT is not for sale");
require(msg.value == _nftPrices[tokenId], "Incorrect Ether sent");
address seller = ownerOf(tokenId);
_transfer(seller, msg.sender, tokenId);
payable(seller).transfer(msg.value);
uint256 indexToRemove = 0;
for(uint256 i = 0; i < _ownedTokens[seller].length; i++) {
if(_ownedTokens[seller][i] == tokenId) {
indexToRemove = i;
break;
}
}
_ownedTokens[seller][indexToRemove] = _ownedTokens[seller][_ownedTokens[seller].length - 1];
_ownedTokens[seller].pop();
_ownedTokens[msg.sender].push(tokenId);
_isForSale[tokenId] = false;
emit NFTSold(tokenId, msg.sender, seller, msg.value);
}
}
작성한 코드는 컴파일을 통해 ABI 파일을 생성하고, Goerli Network에 배포하여 배포된 컨트랙트 주소를 클라이언트에서 사용한다.
Truffle 환경 구축, 컴파일, 배포 참고자료: Truffle - 스마트 컨트랙트 배포 에러 정리
Solidity 이더리움 토큰 관련 내용 참고자료: [Solidity] ERC-721, ERC-1155 / [Solidity] ERC-20
블록체인은 NFT 토큰에 저장한 이미지 정보를 통해 해당 이미지의 소유권이 토큰 소유자에게 있음을 증명할 수 있다. 하지만, NFT 토큰에 저장된 이미지 정보는 변경되지 않지만, 해당 이미지가 저장된 경로가 변경된다면 NFT 토큰 가치가 변경될 수 있는 문제점이 있다. 따라서, 일반적으로 NFT 토큰은 저장하는 이미지 정보에 IPFS를 통해 변경되지 않는 고유의 CID (Content Identifier)를 저장하게 된다.
IPFS에 이미지 파일을 업로드하기 위해 Pinata를 이용한다. Pinata는 IPFS에 파일 업로드를 도와주는 플랫폼 서비스이고, 회원가입 후 일정 수의 파일 업로드에 대해서 무료 서비스를 제공한다. (Pinata 사이트)
Pinata 사이트 로그인 후 마이페이지에서 파일을 업로드하거나 IPFS에 업로드한 파일의 정보를 관리할 수 있다.
이후 DApp에서 Pinata SDK를 사용하여 IPFS에 파일을 업로드하기 위해 API Key를 발급받는다.
그리고, 업로드한 파일에 접근할 수 있도록 Gateways 도메인도 확인한다.
위에서 얻은 정보를 토대로 다음과 같이 정리할 수 있다.
1. 발급받은 API Key는 클라이언트에서 Pinata SDK를 이용하여 IPFS에 파일을 업로드한다.
2. IPFS에 파일을 업로드하면 고유의 CID 값을 반환한다.
3. IPFS Gateway + CID를 통해 IPFS 네트워크에 저장된 이미지 파일에 접근할 수 있다.
프로젝트 코드는 아래 Github 링크에서 확인할 수 있습니다.
Front 프로젝트 > https://github.com/jh-cha/my-virtual-animal
스마트 컨트랙트 배포 프로젝트 > https://github.com/jh-cha/my-virtual-animal-truffle