블록체인 Block-Chain - ERC-721 MetaData, NFT 구매 및 판매

dev_swan·2022년 7월 28일
0

블록체인

목록 보기
29/36
post-thumbnail
post-custom-banner

MetaData

  • MetaDataNFT의 핵심으로 JSON 파일로 되어있으며 아래와 같은 내용을 가지고 있습니다.
    • NFT의 이름
    • NFT에 대한 설명
    • 호스팅된 이미지의 링크
    • 특성들
    • 이 외에 어떤 값이든 추가할 수 있지만 OpenSea와 같은 NFT Market 플랫폼에서 요구하는 변수명으로 입력하는 내용만 화면에 보여줄 수 있습니다.

NFT 구매 및 판매 Contract 작성

open-zeppelin 라이브러리를 설치하여 작업하였고 remix IDE를 사용하여 배포 및 테스트를 해볼 것입니다.

NFT를 발행할 Contract

/* SwanToken.sol */

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

import "./node_modules/openzeppelin-solidity/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
import "./node_modules/openzeppelin-solidity/contracts/access/Ownable.sol"; // Owner 관련 파일
import "./node_modules/openzeppelin-solidity/contracts/utils/Strings.sol"; // toString 관련 파일

contract SwanToken is ERC721Enumerable, Ownable {
  uint constant public MAX_TOKEN_COUNT = 1000; // NFT Token의 최대 발행량을 1000으로 한정할 상수 (constant를 사용하여 값을 변경할 수 없습니다)
  uint public mint_price = 1 ether; // ether를 사용하여 10 ** 18을 연산하지않고 표현 할 수 있습니다.
  string public metadataURI;


  constructor(string memory _name, string memory _symbol, string memory _metadataURI) ERC721(_name,_symbol){
    // 배포를 진행할 때 MetaData의 baseURI를 인자값으로 받아서 사용할 것입니다.
    metadataURI = _metadataURI;
  }

  struct TokenData {
    uint Rank;
    uint Type;
  }

  // tokenId => 구조체 TokenData
  mapping(uint => TokenData) public TokenDatas;

  // Rank와 Type의 뽑힌 갯수를 보여주기 위해 tokenCount를 2차 배열로 만듭니다 .
  uint[4][4] public tokenCount;
}
  • 설치한 open-zeppelin 라이브러리에서 필요한 파일들을 import하여 상속받거나 import한 파일의 함수를 사용하였습니다.
  • Token의 최대 발행량의 제한을 두기 위해 constant 속성을 사용하여 값을 변경 못하도록 상수로 만들어주고 최대 발행량을 1000으로 제한하였습니다.
  • 생성자 함수를 통해 인자값으로 발행할 토큰의 이름, 심볼명, json파일인 metadataURI를 인자값으로 받고 토큰 이름과 심볼명을 가지고 새로운 토큰을 만들어주었고, 인자값으로 받은 _metadataURI을 미리 선언한 metadataURI에 할당하여 주었습니다. 이 metadata URI는 후에 많은 metadata들의 URIbaseURI 역할을 할 것입니다.
  • 발행한 NFT마다 다른 json파일을 가리키기위해 TokenData 구조체를 만들어줍니다. 후에 RankType으로 서로 다른 metadata URI를 가르키도록 처리할 것입니다.
  • tokenId에 따른 TokenData를 알고 값을 입력해주기 위해 TokenDatas mapping을 만들어줍니다.
  • 각각의 NFT들이 RankType마다 몇개씩 발행되었는지 사용자에게 알려주기 위해 2차 배열로 tokenCount 상태변수를 만들었습니다

// 실질적으로 ETH를 받고 새로운 NFT를 발행할 함수
function mintToken() public payable{

  // 받은 ETH가 최소 가격과 같은지 검증.
  require(msg.value == mint_price);

  // NFT를 1000개 이상 발행하려고하면 False
  require(MAX_TOKEN_COUNT > totalSupply());

  uint tokenId = ERC721Enumerable.totalSupply() + 1;

  // TokenData 구조체 타입을 가진 TokenDatas 매핑의 value값을 getRandomNum 함수를 통해 구해옵니다.
  TokenDatas[tokenId] = getRandomNum(msg.sender, tokenId);

  // Rank 1, Type 1 이란 NFT가 만들어졌다면 해당 위치의 값을 1 증가 시킵니다.
  tokenCount[TokenDatas[tokenId].Rank - 1][TokenDatas[tokenId].Type - 1] += 1;

  // Contract를 배포한 EOA 계정에게 바로 받은 ETH를 보내줄 것입니다.
  payable(Ownable.owner()).transfer(msg.value);

  _mint(msg.sender, tokenId);
}
  • function mintToken
    • 실제로 ETH를 받고 NFT를 발행해줄 mintToken 함수를 만들어줍니다. ETH에 대한 거래를 포함하는 함수이니 payable 함수로 만들어줍니다.
    • require()를 사용하여 받은 ETHNFT를 발행할 ETH와 같은지 확인하고 NFT를 1000개까지만 발행하도록 검증하는 코드를 추가하였습니다.
    • NFT의 고유한 값인 tokenId는 총 발행된 NFT의 갯수의 + 1을하여 최초 발행시에는 1, 그후로 1씩 Auto Increment 처리하였습니다.
    • 발행한 NFT마다 metadata의 구분을 짓기 위한 RankTypegetRandomNum 함수를 사용하여 랜덤한 값으로 할당받도록 처리하였습니다. 뒤에서 getRandomNum 함수의 code를 살펴보도록 하겠습니다.
    • 발행한 NFTRank 1, Type 1 이란 metadataURI를 가진 NFT로 만들어졌다면 2차 배열인 tokenCount의 해당 RankType에 위치한 indexCount를 + 1하여 Rank 1, Type 1이란 NFT가 1개 발행되었다고 알 수 있도록 하였습니다.
    • NFT를 발행할 때 받은 ETHCA계정이 아닌 해당 Contract를 배포한 EOA계정에게 바로 ETH를 보내주었습니다.
    • 마지막으로 _mint 함수를 사용하여 함수를 실행한 계정에게 발행한 NFT의 소유권을 할당하였습니다.

function tokenURI(uint _tokenId) public override view returns(string memory) {
  // IPFS -> 파일을 블록체인에 저장하는 느낌
  // IPFS를 쉽게 해주는 웹 사이트 Pinata를 사용할 것입니다.

  // openzeppelin 라이브러리를 사용하여 uint Type을 byte Type으로 변환후 해당 값을 다시 string Type으로 변환해줍니다.
  string memory Rank = Strings.toString(TokenDatas[_tokenId].Rank);
  string memory Type = Strings.toString(TokenDatas[_tokenId].Type);

  // encodePacked를 사용하여 string을 다시 이어붙여 주고 string type으로 변환해줍니다.
  return string(abi.encodePacked(metadataURI,"/",Rank,"/",Type,".json"));
}
  • function tokenURI
    • open-zeppelin에서 import해온 Strings 파일의 toString 함수를 사용하여 uint Type으로 반환되는 RankType의 값을 byte code로 변환 후 다시 string type으로 변환하여 memory에 저장해주었습니다.
    • tokenIdRandom 값인 RankType으로 NFTmetadatajson파일의 URI값을 만들어 반환해주었습니다.

// TokenData를 Random으로 생성해 줄 함수
function getRandomNum(address _owner, uint _tokenId) private pure returns(TokenData memory){
  // encodePacked는 Type과 상관없이 인자값에 있는 두 값을 이어붙여 줍니다
  // keccak256로 해쉬화 한 값이 uint type이 아니니 앞에 uint로 타입을 변환해줍니다.
  // %100을 사용하여 뒤에 두자리수만 뽑아옵니다.
  uint randomNum = uint(keccak256(abi.encodePacked(_owner, _tokenId)))%100;

  // getRandomNum 함수 안에서 TokenData Type을 가진 임시 data를 생성해줍니다.
  TokenData memory data;

  /**
        data = {
            Rank:uint,
            Type:uint,
        }
        */

  if (randomNum < 5) {
    // Rank = 4
    if (randomNum == 1) {
      data.Rank = 4;
      data.Type = 1;
    } else if (randomNum == 2) {
      data.Rank = 4;
      data.Type = 2;
    } else if (randomNum == 3) {
      data.Rank = 4;
      data.Type = 3;
    } else {
      data.Rank = 4;
      data.Type = 4;
    }
  } else if (randomNum < 13) {
    // Rank = 3
    if (randomNum < 7) {
      data.Rank = 3;
      data.Type = 1;
    } else if (randomNum < 9) {
      data.Rank = 3;
      data.Type = 2;
    } else if (randomNum < 11) {
      data.Rank = 3;
      data.Type = 3;
    } else {
      data.Rank = 3;
      data.Type = 4;
    }
  } else if (randomNum < 37) {
    // Rank = 2
    if (randomNum < 19) {
      data.Rank = 2;
      data.Type = 1;
    } else if (randomNum < 25) {
      data.Rank = 2;
      data.Type = 2;
    } else if (randomNum < 31) {
      data.Rank = 2;
      data.Type = 3;
    } else {
      data.Rank = 2;
      data.Type = 4;
    }
  } else {
    // Rank = 1
    if (randomNum < 52) {
      data.Rank = 1;
      data.Type = 1;
    } else if (randomNum < 68) {
      data.Rank = 1;
      data.Type = 2;
    } else if (randomNum < 84) {
      data.Rank = 1;
      data.Type = 3;
    } else {
      data.Rank = 1;
      data.Type = 4;
    }
  }

  return data;
}
  • function getRandomNum
    • abiencodePacked 메서드를 사용하여 Type과 상관없이 인자값의 값들을 이어붙이고 그 값을 keccak256 메서드를 사용하여 해쉬화하고 그값을 다시 uint type으로 변환해주고 % 100으로 그 값의 마지막 두자리만 가져옵니다.
    • TokenData Typedatamemory로 선언하고 if문으로 일정 확률로 RankType의 값을 할당해주고 해당 값을 return하였습니다.
    • 이렇게 Random한 요소로 생성돠는 NFTRankType으로 구매자의 NFT metadata를 마치 뽑기처럼 랜덤으로 처리해주었습니다.

// MetaData URI 수정하는데 Contract를 배포한 사람만 수정을 가능하게 합니다.
function setMetaDataURI(string memory _uri) public  onlyOwner  {
  metadataURI = _uri;
}

// tokenId를 넣었을때 해당 NFT의 Rank를 return합니다.
function getTokenRank(uint _tokenId) public view returns(uint) {
  return TokenDatas[_tokenId].Rank;
}

// tokenId를 넣었을때 해당 NFT의 Type를 return합니다.
function getTokenType(uint _tokenId) public view returns(uint) {
  return TokenDatas[_tokenId].Type;
}

// tokenCount를 보여주는 함수
function getTokenCount() public view returns(uint[4][4] memory) {
  return tokenCount;
}
  • function setMetaDataURI
    • 혹시 후에 metadata URI를 수정해야 할 수도있으니 metadata URI을 수정하는 함수를 만들어줍니다. onlyOwner 속성을 사용하여 해당 함수는 Contract를 배포한 사람만 수정할 수 있도록 처리하였습니다.
  • function getTokenRank
    • tokenId를 입력하였을 때 해당 NFTRankreturn해줍니다.
  • function getTokenType
    • tokenId를 입력하였을 때 해당 NFTTypereturn해줍니다.
  • function getTokenCount
    • NFT들이 RankType마다 몇개씩 발행되었는지를 알 수 있는 tokenCountreturn해줍니다. 굳이 새로운 getter 함수를 만드는 이유는 기존 배열의 getter 함수는 하나의 값만 return 해주었지만 지금은 배열의 모든 값을 확인해야 하는 상황입니다. 이같은 상황에서 memory 속성을 return 값에 추가하면 함수 내부적으로 배열의 모든값들을 memory에 새로 추가하여 그 값을 return해주는 것 같습니다.

NFT를 구매 및 판매할 Contract

/*  */

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

import "./SwanToken.sol";

contract SaleToken {
  SwanToken public Token;

  constructor(address _tokenAddress) {
    // SwanToken이 배포된 후 SaleToken Contract에서 SwanToken Contract를 사용할 수 있도록 처리해줍니다.
    Token = SwanToken(_tokenAddress);
  }

  struct TokenInfo {
    uint tokenId;
    uint Rank;
    uint Type;
    uint price;
  }

  // NFT 가격 매핑
  // tokenId => Price
  mapping(uint => uint) public tokenPrices;

  // 판매중인 NFT 
  // tokenId값이 들어갑니다.
  uint[] public SaleTokenList;
}
  • NFT를 발행할 SwanToken ContractCA값을 생성자 함수 인자값에 넣어 Token이라는 상태 변수에 할당하여 SwanToken Contract의 메서드들을 사용할 수 있도록 하였습니다.
  • NFT의 메타 데이터에 들어가는 내용들을 화면에 보여주기 편하도록 TokenInfo라는 구조체를 선언하였습니다.
  • 마지막으로 NFT의 판매가를 설정할 tokenPrices 매핑과 NFT의 판매중 여부를 판단하는 SaleTokenList 배열을 선언하였습니다.

// NFT 판매 등록 함수
function SalesToken(uint _tokenId, uint _price) public {

  // NFT의 소유자를 가져옵니다.
  address tokenOwner = Token.ownerOf(_tokenId);
  // NFT의 소유자 계정과 함수를 실행한 계정이 비교하여 같은지 확인합니다.
  require(tokenOwner == msg.sender);

  require(_price > 0);

  // NFT판매자의 대리인이 맞는지 확인합니다.
  require(Token.isApprovedForAll(msg.sender, address(this)));

  tokenPrices[_tokenId] = _price;
  SaleTokenList.push(_tokenId);
}
  • function SalesToken
    • NFT보유자가 NFT를 판매 등록할 함수를 만들어줍니다. NFT의 소유자를 가져오고 소유자와 함수를 실행한 사람이 같은지 확인하고 Token에 있는 isApprovedForAll 메서드를 사용하여 SaleTokenCA값이 NFT 판매자의 대리인이 맞는지 확인합니다.
    • 위의 검증을 모두 통과하였으면 tokenPrices에 해당 NFTtokenId값과 판매가를 할당하고 판매중인 NFT 목록인 SaleTokenList에 판매할 tokenId값을 넣어줍니다.

// NFT 구매 함수
function BuyToken(uint _tokenId) public payable {
  // NFT의 소유자를 가져옵니다.
  address tokenOwner = Token.ownerOf(_tokenId);
  // 본인이 본인 NFT를 사진 않음.
  require(tokenOwner != msg.sender);
  // NFT의 판매 가격이 있을경우에 해당 NFT는 판매중인것으로 확인합니다.
  require(tokenPrices[_tokenId] > 0);
  // 구매자가 돈을 알맞게 보냈는지 확인합니다.
  require(tokenPrices[_tokenId] <= msg.value );

  // CA계정으로 받은 ETH를 Contract의 배포자 EOA에게 보내줍니다.
  payable(tokenOwner).transfer(msg.value);

  // 판매완료된 NFT를 대리인이 구매자에게 보내줍니다.
  Token.transferFrom(tokenOwner, msg.sender, _tokenId);

  tokenPrices[_tokenId] = 0;

  popSaleToken(_tokenId);
}
  • function BuyToken
    • NFT 판매 등록함수를 만들었으니 이제 NFT를 구매할 함수를 만들어줍니다. 구매하려는 NFTtokenId값을 가지고 NFT의 소유자를 가져온 후 구매자와 NFT 판매자가 혹시 같은지 확인합니다. 또한 구매자가 구매하려는 NFT가 현재 판매중인지 확인하고 마지막으로 구매자가 NFT 판매가에 알맞게 ETH를 보냈는지 확인합니다.
    • 위의 검증을 모두 통과할 경우 NFT는 환불 기능이 없으니 바로 CA계정에서 받은 ETHSaleToken Contract를 배포한 EOA계정에게 보내줍니다.
    • 이제 판매중이던 NFT를 판매 대리인이 구매자에게 보내줍니다. 판매가 완료된 NFT니 판매중인 NFT 매핑인 tokenPrices 매핑에서 해당 tokenId에 0을 할당하여 판매중이지 않은 NFT로 처리합니다.
    • 마지막으로 판매중인 NFT 배열에서 판매 완료된 NFT를 제거하기 위해 popSaleToken 함수를 실행합니다. 해당 함수에 대한 해석은 뒤에서 설명하겠습니다.

// NFT 판매취소를 처리할 함수
function cancleSaleToken(uint _tokenId) public {
  // NFT의 소유자를 가져옵니다.
  address tokenOwner = Token.ownerOf(_tokenId);
  require(tokenOwner == msg.sender);
  require(tokenPrices[_tokenId] > 0);

  tokenPrices[_tokenId] = 0;
  popSaleToken(_tokenId);
}
  • function cancleSaleToken
    • NFT 판매등록을 하고 취소하고 싶을수도 있으니 NFT 판매 취소를 처리할 함수를 만들어줍니다. 먼저 NFT의 소유자를 가져오고 소유자와 함수를 실행한 계정과 같은지 확인하고 해당 NFT가 판매중이던 상품이었는지 확인합니다.
    • NFT 판매 목록 매핑인 tokenPrices에서 해당 tokenId에 0을 할당하여 판매중이지 않은 NFT로 처리합니다
    • 마지막으로 판매중인 NFT 목록 배열에서 해당 NFT를 제거하기위해 popSaleToken 함수를 실행합니다. 해당 함수에 대한 해석은 뒤에서 설명하겠습니다.

// 판매중인 NFT 배열에서 NFT를 제거할 함수
function popSaleToken(uint _tokenId) private returns(bool) {
  for(uint i = 0; i < SaleTokenList.length; i++) {
    if(SaleTokenList[i] == _tokenId) {
      // SaleTokenList 안에서 tokenId와 일치하는 index를 찾고 해당 index의 값을 배열 마지막 index의 값으로 변경합니다.
      SaleTokenList[i] = SaleTokenList[SaleTokenList.length-1];
      // 배열의 마지막 index의 값을 제거해주면 끝입니다.
      SaleTokenList.pop();
      return true;
    }
  }
  return false;
}
  • function popSaleToken
    • 판매가 완료되거나 판매를 취소할때 판매중인 NFT 배열에서 해당 NFT를 제거하는 함수를 만들어줍니다.
    • 반복문을 사용하여 판매중인 NFT 목록인 SaleToken안에 모든 NFT들을 확인하여 판매 목록에서 제거할 NFTtokenId와 일치하는 NFT를 찾으면 해당 index에 위치한 값을 배열 마지막 index의 값으로 변경합니다. 이렇게하면 배열의 마지막 index의 위치했던 tokenId값은 2개가 되고 제거해야할 tokenId값은 사라지게 되었을것입니다. 이제 pop 메서드를 사용하여 배열의 마지막 indextokenId값을 제거하여 중복된 tokenId값을 하나 제거합니다.

// 전체 판매중인 NFT토큰 리스트를 return합니다.
function getSaleTokenList() public view returns(TokenInfo[] memory){
  require(SaleTokenList.length > 0);
  // 판매중인 NFT 토큰 리스트와 같은 길이의 배열 생성
  TokenInfo[] memory list = new TokenInfo[](SaleTokenList.length);

  for(uint i = 0; i < SaleTokenList.length; i++) {
    uint tokenId = SaleTokenList[i];
    uint Rank = Token.getTokenRank(tokenId);
    uint Type = Token.getTokenType(tokenId);
    uint price = tokenPrices[tokenId];

    list[i] = TokenInfo(tokenId, Rank, Type, price);
  }

  return list;
}
  • function getSaleTokenList
    • 판매중인 모든 NFT들을 화면에 보여줄 함수를 만들어줍니다. TokenInfo type에 맞는 list라는 새로운 배열을 선언하는데 memory 속성을 사용하여 해당 함수 내에서만 사용하도록 하였고 판매중인 NFT 목록인 SaleTokenList와 같은 길이로 생성하였습니다.
    • 판매중인 NFT 목록인 SaleTokenList을 모두 확인하여 tokenId값을 가져온뒤 해당 Token에 해당하는 RankTypeSwanToken Contract에서 생성한 메서드들을 가지고 각각 구해오고 해당 NFT의 판매가격을 가지고 있는 매핑을 통해 해당 tokenIdprice를 구해옵니다.
    • 구해온 값들을 list 배열안에 새로 할당합니다. list 배열안에는 각 TokentokenInfo 구조체가 들어갈 것입니다.
    • 마지막으로 각 Token에 정보들이 담긴 list 배열을 return합니다.

// 내가 소유하고 있는 NFT 리스트를 return합니다.
function getOwnerTokens(address _tokenOwner) public view returns(TokenInfo[] memory){
  uint balance = Token.balanceOf(_tokenOwner);

  require(balance != 0);
  // 본인이 소유하고 있는 NFT 토큰 리스트와 같은 길이의 배열 생성
  TokenInfo[] memory list = new TokenInfo[](balance);

  for(uint i = 0; i < balance; i++) {
    uint tokenId = Token.tokenOfOwnerByIndex(_tokenOwner, i);
    uint Rank = Token.getTokenRank(tokenId);
    uint Type = Token.getTokenType(tokenId);
    uint price = tokenPrices[tokenId];

    list[i] = TokenInfo(tokenId, Rank, Type, price);     
  }

  return list;
} 
  • function getOwnerTokens
    • 서비스 이용자가 본인이 소유하고 있는 NFT를 확인하는 함수를 만들어줍니다. 인자값으로 지갑주소를 받고 해당 계정이 가지고 있는 NFT가 있는지 확인합니다.
    • 위의 검증을 마쳤다면 본인의 balance, 즉 본인이 소유하고 있는 NFT 갯수와 같은 길이의 배열을 만들어줍니다. 마찬가지로 함수 내에서만 사용하는 상태변수로 memory 속성을 사용하였습니다.
    • 반복문을 돌려 사용자가 소유하고 있는 NFT를 모두 확인하여 위의 getSaleTokenList 함수와 같은 방식으로 Token의 정보들을 가져오는데 tokenId를 가져올때만 tokenOfOwnerByIndex 함수를 사용하여 소유자의 n번째있는 NFTtokenId를 가져오도록 합시다.
    • 마찬가지로 소유자의 모든 Token 정보가 list 배열에 구조체로 들어갈 것입니다. 마지막으로 각 Token에 정보들이 담긴 list 배열을 return합니다.

// 내가 소유하고 있는 마지막 토큰을 가져옵니다.
function getLatestToken(address _tokenOwner) public view returns(TokenInfo memory) {
  uint balance = Token.balanceOf(_tokenOwner) - 1;
  uint tokenId = Token.tokenOfOwnerByIndex(_tokenOwner,balance);
  uint Rank = Token.getTokenRank(tokenId);
  uint Type = Token.getTokenType(tokenId);
  uint price = tokenPrices[tokenId];

  return TokenInfo(tokenId,Rank,Type,price);
}
  • function getLatestToken
    • 마지막으로 사용자가 NFTMinting할 때 어떤 Metadata를 가진 NFT인지 알 수 있도록 마지막 NFT 정보를 가져오는 함수를 만들어줍니다.
    • 마찬가지로 memory 속성을 사용하고 balanceOf 함수를 사용하여 소유자의 보유한 NFT 갯수를 가져오는데 - 1을 하여 해당 값을 index로하여 tokenId를 가져옵니다.
    • 이제 해당 NFTRank, Type를 구해오고 price는 필요없지만 TokenInfotypeprice가 있으니 넣어주어 해당 구조체를 return합니다.
post-custom-banner

0개의 댓글