Klaytn NFT 개발기(2)

37925 단어 KlaytnNFTKlaytn

OpenZepplin

오픈제플린은 컨트랙트 개발을 쉽게 도와주는 라이브러리이다. ERC20, ERC721에 대한 형식이 다 있어서 사용법을 보고 따라쓰면 된다.

https://github.com/OpenZeppelin/openzeppelin-contracts

ERC721 토큰에 들어가보면 ERC721과 IERC721이 있는데, ERC721은 토큰의 명세가 구현되어있고, IERC721은 인터페이스 역할을 합니다.

아래와 같이 다중상속을 이용해 구현한 것을 확인할 수 있다.


배포하고 테스팅해보기

배포는 Truffle을 사용하고 사용할 네트워크는 로컬환경에서 쉽게 테스팅 해볼 수 있는 ganache를 사용한다.

  • Truffle
    Truffle은 솔리디티 코드를 로컬 환경에서 쉽게 컴파일 하고 배포할 수 있게 해준다.
    Truffle 폴더구조는 아래와 같습니다.

    - contracts는 솔리디티 컨트랙트의 소스파일이 담겨있다.
    - migrantions는 이더리움 네트워크에 배포할 때 사용하는 디렉토리이다.
    - test는 application과 컨트랙트 테스트 파일 디렉토리이다.
    - truffle.js는 트러플 설정파일이다.
  • Ganache
    가나슈는 가상 이더리움 노드를 로컬환경에서 돌릴 수 있다.
    이더리움이나 클레이튼이나 솔리디티를 사용하고 클레이튼은 이더리움에서 포크되었기 때문에 이더리움 가상 네트워크에서 테스트 해봐도 된다.

    ganache-cli를 통한 로컬테스팅 커맨드

트러플로 배포하기 위해서는 아래의 명령어를 사용하면 된다.

truffle migrate --compile-all --reset --network ganache

배포가 끝나면 build폴더에 json형식으로 컨트랙트들이 저장된다.

배포한 컨트랙트의 노드로 들어가기 위해서는 아래의 명령어를 사용하면 된다.

truffle console --network ganache

배포된 컨트랙트의 노드에 들어가 컨트랙트 인스턴스를 받아올 수 있다.

instance 변수에 컨트랙트를 담아올 수 있다. 또한 컨트랙트에 ERC721FULL 컨트랙트를 상속받아와 생성자에 이름과 심볼을 넣었기 때문에 instance.name()으로 이름을 받아올 수 있다.


컨트랙트 토큰 생성 로직

  • ERC721에서 구현된 _mint()를 통해 토큰을 생성한다.
    공식문서에 구현된 내용을 참조해보면, 토큰 아이디, 토큰개수를 매핑을 통해 구현을 해놓았고, 실제 사용은 ERC721을 상속받고 함수에 인자를 넣어주면 된다.
function _mint(address to, uint256 tokenId) internal virtual {
        require(to != address(0), "ERC721: mint to the zero address");
        require(!_exists(tokenId), "ERC721: token already minted");

        _beforeTokenTransfer(address(0), to, tokenId);

        _balances[to] += 1;
        _owners[tokenId] = to;

        emit Transfer(address(0), to, tokenId);
    }

토큰 URL

  • 토큰에 모든 정보를 담을 수 없으니(가스비 때문), 토큰의 정보를 다른곳에 저장해놓고 그 정보의 URL을 넣어주어, 토큰에 메타데이터를 넣어준다.
    (메타데이터를 사용하는 방식이 마치 데이터베이스가 데이터를 저장하는 방식과 매우 흡사하다고 생각한다. 앞부분만 떼어놓고 실제 뒤에 큰 정보는 연결해놓고 사용하는 느낌)
function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
        require(_exists(tokenId), "ERC721Metadata: URI query for nonexistent token");

        string memory baseURI = _baseURI();
        return bytes(baseURI).length > 0
            ? string(abi.encodePacked(baseURI, tokenId.toString()))
            : '';
    }

토큰 테스트

컨트랙트를 다시 재배포하고 실행하였다.

  • mint함수를 통한 토큰 생성
    성공적으로 발행이 되었다면 영수증을 발행한다

  • 토큰 전체 개수 확인하기
    토큰의 전체 개수를 확인해보면 1개로 나온다.


토큰 중복 테스트

컨트랙트 methods안에 이미 구현되어있는 함수를 이용해 중복테스트를 하였다.


토큰 메타 데이터

실제 블록체인을 이용할때는 토큰의 해쉬값만 사용하고 데이터는 메타 데이터에 사용하는데 IPFS라는 분산 네트워크를 사용해 데이터를 저장한다.

https://ipfs.io/#why

메타데이터를 저장하고 받아온 해쉬값이다.

try{
      const metaData = this.getERC721MetadataSchema(videoId,title, `https://img.youtube.com/vi/${videoId}/mqdefault.jpg`);
      var res = await ipfs.add(Buffer.from(JSON.stringify(metaData)));
      alert(res[0].hash);
    }catch(err) {
      console.error(err);
      spinner.stop();
    }

프론트 토큰 생성 및 가스비 대납

프론트에서 토큰 생성로직은 크게 3가지로 나뉜다.

  1. 메타데이터생성
  2. ipfs에 업로드
  3. 컨트랙트의 mintYTT함수 실행하기

먼저 sender로 로그인한 사용자의 계정을 담아오고, feePayer에 배포 및 대납을 해줄 컨트랙트 계정을 담아온다.

klaytn 네트워크에 가스비를 대신 납부해줄 대납 컨트랙트를 적고 트랜잭션을 보낸다.
트랜잭션이 성공하면 recipt.transactionHash에 영수증이 담긴다.

mintYTT: async function (videoId, author, dateCreated, hash) {    
    const sender = this.getWallet();
    const feePayer = cav.klay.accounts.wallet.add('0xb49458083fbe40c2b0f91f413236e9261e8c6d978701e88a78598be7a773c28c')

    // using the promise
    const { rawTransaction: senderRawTransaction } = await cav.klay.accounts.signTransaction({
      type: 'FEE_DELEGATED_SMART_CONTRACT_EXECUTION',
      from: sender.address,
      to:   DEPLOYED_ADDRESS,
      data: yttContract.methods.mintYTT(videoId, author, dateCreated, "https://ipfs.infura.io/ipfs/" + hash).encodeABI(),
      gas:  '500000',
      value: cav.utils.toPeb('0', 'KLAY'),
    }, sender.privateKey)

    cav.klay.sendTransaction({
      senderRawTransaction: senderRawTransaction,
      feePayer: feePayer.address,
    })
    .then(function(receipt){
      if (receipt.transactionHash) {
        console.log("https://ipfs.infura.io/ipfs/" + hash);
        alert(receipt.transactionHash);
        location.reload();
      }
    });
  },   

사용자가 가스비를 부담하는데 무리가 있으니 커뮤니티 차원에서 일단 가스비를 대납하는 방식으로 진행한다.


사용자 토큰 확인하기

사용자가 어떤 토큰을 가지는지 확인하기 위해선 ERC721Enumerable을 이용한다.
ERC721enumerable에 구현되어있는 tokenOfOwnerByIndex함수를 이용해 계정 토큰을 불러올 수 있다. 내부적으론 사용자 토큰을 배열로 저장해놓고 반복문을 돌면서 필요한 토큰을 반환한다.

https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md

토큰을 mint 했으면 UI에 표시하기 위해 메타데이터로 ipfs에 올라가 있는 데이터를 받아와 UI로 보여주는 작업을 해야한다.

renderMyTokens: function (tokenId, ytt, metadata) {    
    var tokens = $('#myTokens');
    var template = $('#MyTokensTemplate');
    template.find('.panel-heading').text(tokenId);
    template.find('img').attr('src', metadata.properties.image.description);
    template.find('img').attr('title', metadata.properties.description.description);
    template.find('.video-id').text(metadata.properties.name.description);
    template.find('.author').text(ytt[0]);
    template.find('.date-created').text(ytt[1]);

    tokens.append(template.html());
  },

토큰 확인


토큰 사고팔기 컨트랙트

TokenSales 컨트랙트를 만들고 토큰을 사고파는 컨트랙트 로직을 작성하고 가나슈 환경에 배포하였다.


토큰 판매 로직

토큰 소유자를 불러오고 토큰 소유자가 함수를 호출했는지 유효성 검사를 한다.
그리고 제시한 가격이 0보다 큰지 유효성 검사를 하고, 토큰 소유자가 컨트랙트가 내 토큰을 사고팔수 있도록 했는지 확인하는 검사를 하고,
토큰 id에 토큰가격을 매핑한다.

function setForSale(uint256 _tokenId, uint256 _price) public {
    address tokenOwner = nftAddress.ownerOf(_tokenId);
    require(tokenOwner == msg.sender, "caller is not token owner");
    require(_price > 0, "price is zero or lower");
    require(nftAddress.isApprovedForAll(tokenOwner, address(this)), "token owner did not approve TokenSales contract");
    tokenPrice[_tokenId] = _price;
  }

토큰 판매를 걸어놨으면 토큰을 살 수 있게 로직을 만들어주어야 한다.
토큰 구매에 유효성 검사는 두가지가 있다.

  1. 구매자가 클레이가 충분히 있어야 한다.
  2. 토큰 판매자는 자신의 토큰을 구매할 수 없다.

위의 두가지 유효성 검사가 끝나면 판매자 계정으로 돈을 송금하고 구매자 계정으로 토큰을 전송하고 토큰 가격을 0원으로 바꾼다.

function purchaseToken(uint256 _tokenId) public payable {
    uint256 price = tokenPrice[_tokenId];
    address tokenSeller = nftAddress.ownerOf(_tokenId);
    require(msg.value >= price, "caller sent klay lower than price");
    require(msg.sender != tokenSeller, "caller is token seller");
    address payable payableTokenSeller = address(uint160(tokenSeller));
    payableTokenSeller.transfer(msg.value); // 판매자 계정으로 돈 송금
    nftAddress.safeTransferFrom(tokenSeller, msg.sender, _tokenId);
    tokenPrice[_tokenId] = 0;
  }

토큰 판매 취소

소유자는 자신이 판매로 올려놓은 토큰판매를 취소할 수 있어야 한다.
인자로 가지고 있는 토큰을 받고, 토큰의 가격을 0으로 만들어 더이상 판매하는 토큰이 아님을 작성한다.

function removeTokenOnSale(uint256 memory tokenIds) public {
    require(tokenIds.length > 0 , "tokenIds is empty");
    for(uint i = 0; i < tokenIds.length; i++){
      uint tokenId = tokenIds[i];
      address tokenSeller = nftAddress.ownerOf(tokenId);
      require(msg.sender == tokenSeller, "caller is not token seller");
      tokenPrice[tokenId] = 0;
    }
  }

토큰판매 UI

컨트랙트가 판매자의 토큰을 대신 팔수 있도록 approved를 해주고 isApproved로 승인을 받았는지를 받아온다

var isApproved = await this.isApprovedForAll(walletInstance.address, DEPLOYED_ADDRESS_TOKENSALES);

승인이 완료되고, 토큰 판매를 누르면 얼마에 팔지 KLAY를 적을 수 있다.

인자로 받아온 판매값을 통해 tokenSales 컨트랙트의 setForSale함수를 통해 컨트랙트에게 판매 권한을 제공한다.
물론 가스비는 대납 컨트랙트에서 대신 납부한다.

const { rawTransaction: senderRawTransaction } = await cav.klay.accounts.signTransaction({
      type: 'FEE_DELEGATED_SMART_CONTRACT_EXECUTION',
      from: sender.address,
      to:   DEPLOYED_ADDRESS_TOKENSALES,
      data: tsContract.methods.setForSale(tokenId,cav.utils.toPeb(amount, 'KLAY')).encodeABI(),
      gas:  '500000',
      value: cav.utils.toPeb('0', 'KLAY'),
    }, sender.privateKey)

    cav.klay.sendTransaction({
      senderRawTransaction: senderRawTransaction,
      feePayer: feePayer.address,
    })
    .then(function(receipt){
      if (receipt.transactionHash) {
        alert(receipt.transactionHash);
        location.reload();
      }
    });
    }catch(err){
      console.error(err);
      spinner.stop();
    }

승인이 완료되면 얼마에 팔고있다고 알려준다.


토큰 구매

판매토큰에 썻던 로직과 비슷하다. tokenSales 컨트랙트의 purchaseToken함수를 실행하고 인자를 넣어주면 된다.

 const { rawTransaction: senderRawTransaction } = await cav.klay.accounts.signTransaction({
      type: 'FEE_DELEGATED_SMART_CONTRACT_EXECUTION',
      from: sender.address,
      to:   DEPLOYED_ADDRESS_TOKENSALES, 
      data: tsContract.methods.purchaseToken(tokenId).encodeABI(),
      gas:  '500000',
      value: price,
    }, sender.privateKey)


좋은 웹페이지 즐겨찾기