본문 바로가기
블록체인/블록체인 서비스 개발

[NFT 101] Contract 개발

by 마고커 2022. 3. 21.


NFT 개발의 맛만 보기 위해, 가장 기본적인 내용들만 스스로 만들어 보면서 간단히 실행해 본다. Contract를 만들어 배포하고, 간단한 React App으로 Contract를 통해 민팅된 값들을 불러올 것이다. 구체적으로는 제품을 구매 이력을 스마트 컨트랙트로 남기고, 구매 이력을 불러오는 것이다. 모두 불러와서 꾸미는 건 귀찮고, 간단히 구매한 제품 이미지만 불러올 것이다(향후 추가 개발 시에 번거롭지 않도록 contract은 모두 구성해 두었다). 아래 그림은 세탁기 1대, 냉장고 1대를 민팅한 것이다.

 

 

Contract를 만들어 배포하는 방법은 아래를 참조한다.

 

 

[NFT 101] Remix 설치와 Contract Compile, 배포

DApp Contract를 개발하는 가장 쉬운 방법은 Remix를 이용하는 것 같다. VSCode 내에서 터미널을 하나 생성하고 아래 명령어를 차례대로 수행해 준다. > npm install -g @remix-project/remixd > Set-ExecutionPol..

magoker.tistory.com

 

Contract의 전체 코드는 아래와 같다. Contract은 중요하니, 간단한 예지만 자세히 이야기한다.

 

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";

contract MintProdToken is ERC721Enumerable {
    constructor() ERC721("RKSProducts", "HAS") {}

    struct ProdTokenData {
        uint256 prodTokenId;
        string serialNo;
        string productType;
        address lastOwner;
        uint256 lastPrice;
        uint256 price;
        uint256 repairedCount;
    }

    mapping(uint256 => string) serialNo;
    mapping(uint256 => string) productType;
    mapping(uint256 => address) lastOwner;
    mapping(uint256 => uint256) lastPrice;
    mapping(uint256 => uint256) price;
    mapping(uint256 => uint256) repairedCount;

    function mintProdToken(string memory _serialNo, string memory _productType, uint256 _price) public {
        uint256 prodTokenId = totalSupply() + 1;

        serialNo[prodTokenId] = _serialNo;
        productType[prodTokenId] = _productType;
        lastOwner[prodTokenId] = msg.sender;
        lastPrice[prodTokenId] = _price;
        price[prodTokenId] = _price;
        repairedCount[prodTokenId] = 0; 

        _mint(msg.sender, prodTokenId);
    }

    function getProdTokens(address _prodTokenOwner) view public returns (ProdTokenData[] memory) {
        uint256 length = balanceOf(_prodTokenOwner);

        require(length != 0, "No Purchased Item");

        ProdTokenData[] memory prodTokenData = new ProdTokenData[](length);

        for(uint i=0; i<length; i++) {
            uint256 prodTokenId = tokenOfOwnerByIndex(_prodTokenOwner, i);
            string memory _serialNo = serialNo[prodTokenId];
            string memory _productType = productType[prodTokenId];
            address _lastOnwer = lastOwner[prodTokenId];
            uint256 _lastPrice = lastPrice[prodTokenId];
            uint256 _price = price[prodTokenId];
            uint256 _repairedCount = repairedCount[prodTokenId];

            prodTokenData[i] = ProdTokenData(prodTokenId, _serialNo, _productType, _lastOnwer, _lastPrice, _price, _repairedCount);
        }

        return prodTokenData;
    }
}

 

우선 MIT 라이센스임을 일단 밝혀둔다. 그냥 주석 같은데, 이 문장 빠지면 에러를 낸다. 'pragrma solidity'는 이더리움 컴파일러(solidity) 버전을 나타낸다. 작성 시점으로 0.8.13까지 나와 있다(Releases · ethereum/solidity · GitHub). 그리고, 만들 contract은 NFT(ERC721)임으로 해당 계약서를 import 한다.

 

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";

 

계약서 이름을 MintProdToken으로 정의했고, ERC721 계약서임으로 명시했다. 그러면, ERC721의 기본 함수인 Mint, BalanceOf 등등을 활용할 수 있다. 자세한 함수 목록은 여기(ERC 721 - OpenZeppelin Docs)를 참고 한다. 생성자에 들어가는 두 개의 파라미터는 이름 메타와 심볼인데, 적당한 이름을 지어주면 된다. 다음은 계약서에 포함된 제품 아이템의 속성들을 구조체로 정의한다. 주석을 보면 알겠지만, 구매와 중고 거래 이력을 추적해 당근 마켓에 올라왔을 때, 장물이 아닌 지 확인하는 용도로 계획했었다. 모두 구현해서 올리는 것은 취지에 맞지 않으므로, 클라이언트 애플리케이션에서는 productType만 가져와서 이미지로 보여주는 데 까지만 할 것이다.

 

contract MintProdToken is ERC721Enumerable {
    constructor() ERC721("RKSProducts", "HAS") {}

    struct ProdTokenData {
        uint256 prodTokenId; // 제품 구매 순번
        string serialNo;     // 제품 시리얼 번호
        string productType;  // 제품 타입
        address lastOwner;   // 마지막 소유자
        uint256 lastPrice;   // 중고거래 마지막 가격
        uint256 price;       // 현재 가격
        uint256 repairedCount;  // 수리 이력
    }

 

계약서 문법을 접하고 첫번째 난관은 mapping 함수였다. 어쩌라는 것이냐! 이해하면 간단한데, 변수는 mapping 함수의 첫번째 파라미터 형태의 순번(Key)을 갖는 위치에, 두번째 파라미터 형태의 값(Value)이 들어간다는 뜻이다. 즉, serialNo[0]이나 productType[50]은 string 형태의 값을 갖지만, lastPrice[11]이나 repairedCount[22]는 uint 값을 갖게 된다 정도로 이해하면 된다. 즉, 위의 구조체 정의대로 작성해 주면 된다.

 

    mapping(uint256 => string) serialNo;
    mapping(uint256 => string) productType;
    mapping(uint256 => address) lastOwner;
    mapping(uint256 => uint256) lastPrice;
    mapping(uint256 => uint256) price;
    mapping(uint256 => uint256) repairedCount;

 

계약서의 함수는 두 가지다. Minting과 Minting된  아이템을 불러오는 것. 우선 Minting 부분은 아래와 같다.

 

function mintProdToken(string memory _serialNo, string memory _productType, uint256 _price) public {
        uint256 prodTokenId = totalSupply() + 1;

        serialNo[prodTokenId] = _serialNo;
        productType[prodTokenId] = _productType;
        lastOwner[prodTokenId] = msg.sender;
        lastPrice[prodTokenId] = _price;
        price[prodTokenId] = _price;
        repairedCount[prodTokenId] = 0; 

        _mint(msg.sender, prodTokenId);
    }

 

최초 구매자의 lastOwner는 본인이므로 msg.sender를, 최초구매이므로 lastPrice와 price는 같게, repairedCount는 0이 된다. 즉 _serialNo, _productType, _price만 인자로 받는다.  주의할 점은 string 형태는 반드시 memory에 우선 저장하도록 되어 있으니 명기해 주어야 한다. totalSupply()와 _mint()는 ERC721에 정의되어 있는 함수로, 현재까지의 총 발행량을 얻거나, Minting할 때 사용한다. 나머지는 계약서에 mapping된 변수에 다 저장되어 있으므로, Minting 시에는 prodTokenId값만 지정해준다. 계약서 배포 후에 remix에서 민팅해서 아래와 같이 메시지가 나오면 제대로 된 것이다.

 

 

 본 예에서 민팅은 remix에서, 제품 목록을 가져오는 것은 React web에서 할  것이다. 일단 계약서에서 제품 목록을 가져오는 함수를 아래와 같이 구성 해 둔다. balanceOf는 해당 owner의 현재까지 보유한 아이템의 길이, 즉 수량을 가져온다. require 지시자는 해당 조건이 성립하지 않으면, 두번째 파라미터의 에러를 내고 해당 함수를 종료한다. 여기서는 보유한 아이템이 없으면 "No Purchased Item" 메시지를 출력한 후 종료하는 것이다.  for문은 비교적 명시적이다. 인자로 받은 token owner의 아이템들을 순회하면서 prodTokenId를 얻어오고, 해당 token id에 속하는 변수 값들을 가져와, 이를 위에서 정의한 구조체 ProdTokenData로 만들어, 이에 대한 배열을 Return 한다.

 

function getProdTokens(address _prodTokenOwner) view public returns (ProdTokenData[] memory) {
        uint256 length = balanceOf(_prodTokenOwner);

        require(length != 0, "No Purchased Item");

        ProdTokenData[] memory prodTokenData = new ProdTokenData[](length);

        for(uint i=0; i<length; i++) {
            uint256 prodTokenId = tokenOfOwnerByIndex(_prodTokenOwner, i);
            string memory _serialNo = serialNo[prodTokenId];
            string memory _productType = productType[prodTokenId];
            address _lastOnwer = lastOwner[prodTokenId];
            uint256 _lastPrice = lastPrice[prodTokenId];
            uint256 _price = price[prodTokenId];
            uint256 _repairedCount = repairedCount[prodTokenId];

            prodTokenData[i] = ProdTokenData(prodTokenId, _serialNo, _productType, _lastOnwer, _lastPrice, _price, _repairedCount);
        }

        return prodTokenData;
    }

 

계약서는 완료되었으므로, 배포를 해둔 뒤 클라이언트를 작성해서 호출하면 된다. 



댓글