[BlockChain] NFT만들기, NFT거래 컨트랙트

2022. 12. 15. 16:52·개발/BlockChain
728x90
반응형

오픈제플린 표준규격을 활용하여 컨트랙트 구성

1. NFTToken.sol

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

import "../node_modules/openzeppelin-solidity/contracts/token/ERC721/extensions/ERC721Enumerable.sol";

import "../node_modules/openzeppelin-solidity/contracts/access/Ownable.sol";

import "../node_modules/openzeppelin-solidity/contracts/utils/Strings.sol";

contract NFTToken is ERC721Enumerable, Ownable{
    // NFT 발행량을 제한 할 상태 변수
    uint constant public MAX_TOKEN_COUNT = 1000;
	// NFT민팅시 지불할 이더 민팅의 가격을 나타재누는 상태 변수;
    uint public mint_price = 1 ether;
    
    string public metadataURI;
    constructor(string memory _name, string memory _symbol, string memory _metadataURI) ERC721(_name,_symbol){
        metadataURI = _metadataURI;
   }

   // token 구조체 생성
   struct TokenData{
    uint Rank;
    uint Type;
   }

   // uint 키값은 토큰의 ID
   mapping(uint=>TokenData) public TokenDatas;

   // 어떤 타입의 토큰이 몇개나 발행 되었는지 확인하기 위한 상태 변수
   uint [4][4] public tokenCount;

   function mintToken() public payable {
     // mintToken을 하면 이더를 지급하게 만들고 CA에게 이더를 보내서 NFT를 구매한다.
     // 본인의 잔액이 mint_price보다 클때
     require(msg.value> mint_price);
     // 총발행량이 MAX_TOKEN_COUNT보다 작을때
     require(MAX_TOKEN_COUNT > ERC721Enumerable.totalSupply());

     // 총발행량에 1을 더해서 도큰 아이디 변수에 담아둠
     uint tokenId = ERC721Enumerable.totalSupply() + 1;

     // tokenId에 맞춰서 Rank랑 Type을 랜덤 생성해서 TokenData로 저장
     // 랜덤 숫자 생성
     TokenData memory random = getRandomTokenData(msg.sender, tokenId); 
     // 총 발행량의 +1 더해진 토큰 아이디
     TokenDatas[tokenId] = random;

     // 랜덤으로 생성한 Rank와 Type을 가진 Token의 갯수가 몇개인지 확인하기 위한 상태 변수
     tokenCount[TokenDatas[tokenId].Rank-1][TokenDatas[tokenId].Type-1] += 1;

     //CA => 컨트랙트 배포자 계정에 지급받은 이더를 전송해준다.
     payable(Ownable.owner()).transfer(msg.value);

     // mintToken함수를 호출한 계정에 NFT발행
     _mint(msg.sender, tokenId);
   }

    // TokenData를 랜덤하게 만들어줄 함수
   function getRandomTokenData(address _owner, uint _tokenId) private pure returns(TokenData memory){
        // 솔리디티에서는 랜덤함수를 만들수 없기 때문에 특정한값을 해싱해서 나머지 연산으로 랜덤을 구해야 한다.
        // abi.encodePacked(_owner, _tokenId); -> 타입과 상관없이 합쳐주는 메소드
        uint randomNum = uint(keccak256(abi.encodePacked(_owner, _tokenId)))%100;

        // 토큰의 데이터를 저장할 변수
        TokenData memory data;

        if(randomNum < 5){
            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){
            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){
            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{
            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 tokenURI (uint _tokenId) public override view returns(string memory){
    // uint에서 바로 문자열로 형변환이 불가능하기 때문에 uint에서 bytes로 변환하고 문자열로 형변환 해준다.
    string memory Rank = Strings.toString(TokenDatas[_tokenId].Rank);
    string memory Type = Strings.toString(TokenDatas[_tokenId].Type);

    // http://localhost:3000/metadata/1/3/.json 이런 형태로 만들어 줄것
    return string(abi.encodePacked(metadataURI, "/",Rank,"/",Type,".json"));


   }
    // metadataURI을 수정해야 할때 사용할 함수
    // onlyOwner : 컨트랙트 배포자만 호출할수 있는 함수
    function setMetadataURI(string memory _uri) public onlyOwner{
        metadataURI = _uri;
    }

    // TokenData의 Rank를 조회하는 함수
    function getTokenRank(uint _tokenId) public view returns(uint){
        return TokenDatas[_tokenId].Rank;
    }
    function getTokenType(uint _tokenId) public view returns(uint){
        return TokenDatas[_tokenId].Type;
    }

    // 배열전체 내용을 조회하는 함수
    // 솔리디티에서 배열을 getter로 전체 조회하는건 불가능한데 요소하나만 return 하기 때문에 
    // 배열천제를 return해주는 view함수를 만들어 주면 된다.
    function getTokenCount() public view returns(uint[4][4] memory){
        return tokenCount;
    }
}

2. SaleToken.sol

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

import "./NFTToken.sol";

contract SaleToken{
    // 상태 변수인데 NftToken과 상호작용을 하기 위해서 만든 상태변수
    NFTToken public Token;

    constructor(address _tokenAddress){
        Token = NFTToken(_tokenAddress);
    }

    // Token info 구조체
    struct TokenInfo{
        uint tokenId;
        uint Rank;
        uint Type;
        uint price;
    }

    // token의 아이디 => price(토큰의 가격);
    mapping(uint => uint) public tokenPrices;

    //판매중인 nft의 토큰아이디 값을 담아놓은 상태변수/
    uint[] public SaleTokenList;

    // 판매등록함수
    function SalesToken(uint _tokenId, uint _price) public {
        // 토큰의 소유자 계정
        address tokenOwner = Token.ownerOf(_tokenId);

        // 토큰의 소유자를 가져왔을때 소유자가 맞으면 판매 가능
        require(tokenOwner == msg.sender);

        // 판매 가격이 0보다 큰값인지 확인
        require(_price > 0);

        // SalesToken() 함수를 실행한 사람이 모든 토큰의 권한을 위임했는지 확인
        require(Token.isApprovedForAll(msg.sender, address(this)));

        // 토큰의 가격을 토큰아이디 인덱스에 가격 추가
        tokenPrices[_tokenId] = _price;

        // 판매 리스트에 토큰 아이디 추가
        SaleTokenList.push(_tokenId);
    }

    // 토큰 구매 함수
    function PurchaseToken(uint _tokenId) public payable{
        address tokenOwner = Token.ownerOf(_tokenId);

        // 판매자가 자신의 토큰을 구매 못하게
        require(tokenOwner != msg.sender);

        // 판매중인 토큰만 구매할 수 있게 체크
        // 여기서 tokenPrices의 값이 0이상인 경우 판매중으로 인식
        require(tokenPrices[_tokenId] > 0);

        // 구매자가 지불한 이더가 판매 가격 이상인지 체크
        require(tokenPrices[_tokenId] < msg.value);

        // CA가 토큰 판매자에게 이더 전송
        payable(tokenOwner).transfer(msg.value);

        // 토큰 전달
        Token.transferFrom(tokenOwner,msg.sender, _tokenId);

        // 판매가격을 0으로 만들면 상품 판매중이지 않게 된다.
        tokenPrices[_tokenId] = 0;
        popSaleToken(_tokenId);
    }

    // 판매리스트 제거 함수
    function popSaleToken(uint _tokenId) private returns (bool){
        for(uint i = 0; i < SaleTokenList.length; i++){
            if(SaleTokenList[i] == _tokenId){
                SaleTokenList[i] = SaleTokenList[SaleTokenList.length - 1];
                SaleTokenList.pop();
                return true;
            }
        }
        return false;
    }

    // 전체 판매 리스트 확인 전체 확인
    function getSaleTokenList() public view returns(TokenInfo[] memory){
        // 리스트에 길이가 있을때
        require(SaleTokenList.length > 0);

        // SaleTokenList 리스트의 길이 만큼 빈값을 가지게 배열을 만듬
        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;
    }

    // 소유하고 있는 NFT리스트 view함수
    function getOwnerToken(address _tokenOwner) public view returns (TokenInfo[] memory){
        uint balance = Token.balanceOf(_tokenOwner);
        require(balance != 0);
        // balance크기의 빈배열 만들기 list
        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;
    }

    // 소유하고 있는 제일 최신의 NFT를 보여주는 view함수 민팅했을때 바로 보여주기 위한 용도로 사용
    function getLatestToken(address _tokenOwner) public view returns (TokenInfo memory){
        uint balance = Token.balanceOf(_tokenOwner);
        uint tokenId = Token.tokenOfOwnerByIndex(_tokenOwner, balance-1);
        uint Rank = Token.getTokenRank(tokenId);
        uint Type = Token.getTokenType(tokenId);
        uint price = tokenPrices[tokenId];

        return TokenInfo(tokenId,Rank,Type,price);
    }
}

3. 2_deploy_SaleToken.js

const NFTToken = artifacts.require("NFTToken");
const SaleToken = artifacts.require("SaleToken");

module.exports = async (deployer) => {
  // 이름, 심볼, 메타데이터 URI
  // http://localhost:3000/metadata 이런 json 파일 있는 url
  await deployer.deploy(NFTToken, "MyNFT", "MFT", "url넣는곳");
  // NFTToken 배포를 진행하고 배포 완료된 인스턴스 가져오기
  const token = await NFTToken.deployed();

  // CA값을 넣어주기 위해 NFTToken을 먼저 배포
  await deployer.deploy(SaleToken, token.address);
  // 배포 완료된 인스턴스 가져오기
  const saleToken = await SaleToken.deployed();
};

 

728x90
반응형

'개발 > BlockChain' 카테고리의 다른 글

[BlockChain] 블록체인의 트릴레마  (0) 2023.02.05
[BlockChain] 블록체인이란 무엇인가?  (0) 2023.01.24
[ERC토큰] ERC20 ERC721 ERC777 ERC1155  (0) 2022.12.12
[BlockChain] NFT 만들기 (goerliETH)  (0) 2022.12.06
[BlockChain] localhost에서 remix 연동  (0) 2022.12.06
'개발/BlockChain' 카테고리의 다른 글
  • [BlockChain] 블록체인의 트릴레마
  • [BlockChain] 블록체인이란 무엇인가?
  • [ERC토큰] ERC20 ERC721 ERC777 ERC1155
  • [BlockChain] NFT 만들기 (goerliETH)
TeTedo.
TeTedo.
  • TeTedo.
    TeTedo 개발 일기
    TeTedo.
  • 전체
    오늘
    어제
    • 분류 전체보기 (319)
      • 개발 (274)
        • Article (4)
        • 정리 (21)
        • Spring Boot (17)
        • JPA (2)
        • JAVA (6)
        • Database (4)
        • 자료구조 (11)
        • 알고리즘 (32)
        • React (20)
        • Docker (10)
        • node.js (18)
        • Devops (11)
        • Linux (4)
        • TypeScript (3)
        • Go (10)
        • HyperLedger (4)
        • BlockChain (43)
        • html, css, js (48)
        • CS (3)
        • AWS (3)
      • 모아두고 나중에 쓰기 (3)
      • 팀프로젝트 (18)
        • SNS(키보드워리어) (9)
        • close_sea (9)
      • 개인프로젝트 (1)
        • Around Flavor (1)
        • CHAM (13)
        • ethFruitShop (5)
      • 독서 (0)
        • 스프링부트와 AWS로 혼자 구현하는 웹 서비스 (0)
  • 블로그 메뉴

    • 홈
    • 개발일기
    • CS
    • 실습
    • 코딩테스트
    • 웹
    • Go
    • node.js
    • 팀플
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    하이퍼레저
    go
    node
    js
    ERC721
    React
    html
    30일 챌린지
    node.js
    30일챌린지
    도커
    컨테이너
    erc20
    CSS
    프로그래머스
    명령어
    블록체인
    go언어
    nodejs
    mysql
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.1
TeTedo.
[BlockChain] NFT만들기, NFT거래 컨트랙트
상단으로

티스토리툴바