Technology, Tutorials, for Developer

Foundry를 사용하여 온체인 동적 SVG NFT 구축하기

소개

NFT는 토큰 ID와 토큰 URI로 식별되며, 메타데이터라고도 합니다. 이 메타데이터는 가장 일반적으로 https URI 형식을 사용해 저장되지만, data URI 형식을 사용해 저장할 수도 있습니다. 

최근 개발자들은 data URI 형식을 활용하여 스마트 콘트랙트 내에 토큰 URI를 직접 저장하는 방법을 고안했으며, 이를 통해 NFT가 온체인 데이터를 읽고, 관련 상태 변경, 사용자 작업, 외부 활동을 조회하고, 그에 따라 표시되는 내용을 동적으로 수정할 수 있게 되었습니다.

이 튜토리얼에서는 동적 NFT에 대해 알아보고 그 사용법을 알려드리겠습니다:

  • 데이터 URI 형식을 사용하여 온체인 SVG 이미지와 JSON 객체 저장하기
  • Foundry를 사용하여 동적 SVG NFT 컨트랙트 배포 및 상호작용하기
  • 상호작용에 따라 메타데이터를 수정하여 NFT의 동적 기능 살펴보기
  • OpenSea에서 SVG NFT 보기
참고: 토큰 URI(SVG 이미지 및 기타 속성)를 온체인에 직접 저장하는 것은 많은 가스를 사용합니다. 다행히 클레이튼의 가스비는 매우 저렴합니다.

동적 NFT란?

NFT는 수집가에게 진위 여부와 소유권 확인을 쉽게 제공하는 정적 디지털 아트워크로 가장 잘 알려져 있지만, NFT는 스마트 컨트랙트를 활용해 동일한 토큰 ID를 유지하면서 온체인 또는 오프체인 데이터를 기반으로 메타데이터를 편집할 수 있습니다. 이를 통해 비디오 게임 진행, 판타지 스포츠 리그, 실제 자산의 토큰화 등 NFT를 무수히 다양하게 활용할 수 있습니다.

시작하기

시작하기 전에 이 동적 NFT 튜토리얼에서 SVG를 활용하는 이유를 이해하는 것이 중요합니다.

SVG(확장 가능한 벡터 그래픽)는 수학 공식을 사용해 이미지를 저장하는 이미지 형식입니다. 이 파일 형식은 2차원 그래픽, 차트, 일러스트레이션을 표시하는 데 사용할 수 있으며 품질 저하 없이 크기를 쉽게 조정할 수 있습니다.

SVG의 주요 차이점은 픽셀 기반 이미지 파일과 달리 SVG는 XML로 작성되어 텍스트 정보를 저장할 수 있다는 점인데, 이는 이 튜토리얼의 동적 NFT 기능에 필요한 고유 기능으로, SVG 파일에 임의의 단어 세 개를 조합하여 전달할 수 있기 때문입니다. 또한 호스팅 제공업체 없이도 base64로 변환하여 데이터 URI 형식을 사용하여 웹에 표시할 수 있습니다.

다음은 SVG 파일의 예시입니다:

xml

<svg xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMinYMin meet" viewBox="0 0 350 350">,

<style>.base { fill: black; font-family: serif; font-size: 14px;}</style>,  <rect width="100%" height="100%" fill="white" ry="40" rx="40" style="stroke:#FF2F00; stroke-width:5; opacity:0.9" />

<text x="50%" y="20%" class="base" dominant-baseline="middle" text-anchor="middle">Klaytn</text>

<text x="50%" y="30%" class="base" dominant-baseline="middle" text-anchor="middle">Evm Compatibility</text>

<text x="50%" y="40%" class="base" dominant-baseline="middle" text-anchor="middle">Metamask</text>

<text x="50%" y="50%" class="base" dominant-baseline="middle" text-anchor="middle">Tatum</text>

<text x="50%" y="60%" class="base" dominant-baseline="middle" text-anchor="middle">Beginner</text>

<text x="1.2%" y="90%" class="base" dominant-baseline="middle" text-anchor="left" style="font-size:10px;">Klaytn is an open source public blockchain designed for tomorrow's on-chain world</text>

</svg>

이 튜토리얼에서는 데이터 URI 사양을 사용하여 SVG 이미지와 NFT 속성을 json 메타데이터에 저장하겠습니다. 

데이터 URI 구문을 사용하여 SVG 이미지를 저장하면 다음과 같습니다:

data:image/svg+xml;base64,PHN2ZyB4………

데이터 URI 구문을 사용하여 메타데이터를 저장하면 다음과 같이 표시됩니다:

data:application/json;base64,eyJuYW1
  • data: name of the scheme
  • application/json: content type (can also be image/svg+xml)
  • base64: the type of encoding used to encode the data
  • eyJuYW1:  … – the encoded data (SVG file, Metadata.json file )

예시

// SVG image stored using the data URI format

data:image/svg+xml;base64,<encoded SVG file>

// NFT metadata stored using the data URI format

data:application/json;base64,<encoded metadata.json file>

<encoded metadata.json file> =>

{"name": "SVG NFT #1",

"description": "SVG NFT on Klaytn",

"image":"data:image/svg+xml;base64,<encoded SVG file>"

}

위의 예시에서 볼 수 있듯이, 우리 파일은 base64로 되어 있습니다. 따라서 스마트 컨트랙트에도 솔리디티에서 동일한 base64 구현이 필요하며, 이에 대해서는 나중에 다시 설명하겠습니다.

이 튜토리얼에서는 동적이고 업그레이드 가능한 NFT를 만들 것입니다:

  • ‘Klaytn’이라는 단어
  • 목록에서 무작위로 선택된 Klaytn의 고유 기능 중 하나
  • 목록에서 무작위로 선택된 Klaytn의 생태계 파트너 중 하나
  • 목록에서 무작위로 선택된 Web3 용어 1개
  • NFT의 업그레이드 레벨 – Beginner, Intermediate, 또는 Expert
  • 클레이튼에 대한 짧은 한 줄 설명

NFT의 모습은 다음과 같습니다:

전제 조건

동적 SVG NFT 컨트랙트 생성하기

먼저 파운드리 개발 환경에서 klaytn 컨트랙트 라이브러리를 사용하여 동적 SVG NFT 컨트랙트를 생성해 보겠습니다.

1단계: Installing klaytn-contracts and configuring Solidity Optimizer in foundry.toml 

이 단계에서는 클레이튼에서 안전한 스마트 컨트랙트 개발을 위한 라이브러리인 klaytn-contracts를 설치합니다. 또한 파운드리 개발 환경에서 몇 가지 커스터마이징을 할 것입니다. 

Klaytn 컨트랙트 설치하기

클레이튼 컨트랙트를 설치하려면 프로젝트 폴더 터미널에서 아래 명령어를 실행합니다:

bash

forge install klaytn/klaytn-contracts

설치가 완료되면 lib/klaytn-contractsklaytn-contracts 폴더가 표시됩니다.

참고: 추적되지 않은 git 파일이 있는지 확인하세요.

리맵핑

파운드리를 사용하면 종속성을 리매핑하여 가져오기 쉽게 만들 수 있습니다. 한 가지 방법은 프로젝트의 루트에 remappings.txt 파일을 만드는 것입니다. 파일을 생성한 후 아래 텍스트를 새로 생성한 파일에 붙여넣습니다. 

text

@klaytn/=lib/klaytn-contracts/

이러한 리매핑은 다음을 의미합니다:

  • klaytn-contracts에서 가져오려면 다음과 같이 작성합니다: import “@klaytn/contracts/utils/Counters.sol”;

리매핑에 대해 자세히 알아보려면 이 가이드를 읽어보세요.

foundry.toml에서 솔리디티 옵티마이저 설정하기

솔리디티 컴파일러의 동작을 개선하기 위해 다음 구성을 수행합니다. 이 구성을 활성화하려면 foundry.toml 파일을 열고 파일 끝에 다음을 붙여넣습니다:

bash

optimizer=true

optimizer_runs=200

via-ir=true

각 매개변수의 기능을 간단히 살펴보겠습니다:

  • optimizer: true로 설정하면 솔리디티 옵티마이저를 활성화합니다.
  • optimizer-runs: 실행할 옵티마이저 실행 횟수입니다.
  • via-ir: true로 설정하면 컴파일 파이프라인이 새 IR 옵티마이저를 거치도록 변경합니다. 

solidity docs foundry docs 에서 이러한 용어에 대한 자세한 내용을 확인할 수 있습니다.

2단계: 컨트랙트 종속성 가져오기

이 단계에서는 동적 NFT 컨트랙트 구현을 위해 종속 컨트랙트를 가져옵니다. 컨트랙트는 다음과 같습니다:

  • KIP17URIStorage.sol: 이 컨트랙트는 Klaytn 컨트랙트에서 가져온 KIP17 토큰 컨트랙트의 확장입니다. 토큰 ID를 기반으로 토큰 URI를 관리하는 데 도움이 됩니다.
  • Counters.sol: 이 경우 카운터 컨트랙트는 발행된 KIP17 토큰의 수, 즉 각 토큰의 ID를 추적하는 데 사용됩니다.
  • Strings.sol: 문자열 작업과 관련하여 문자열 라이브러리를 사용하여 주로 uint256을 문자열 표현으로 변환합니다.
  • Base64.sol: 이 컨트랙트는 base64 문자열로 작업하기 위한 함수 집합을 제공합니다.
참고 : Base64 인코딩의 일반적인 응용 분야 중 하나는 바이너리 데이터를 인코딩하여 data: URL에 포함될 수 있도록 인코딩하는 것입니다. 앞서 데이터 URI 형식을 사용해 NFT 메타데이터를 온체인에 저장할 수 있다고 설명드린 것을 기억하시나요? 이 Base64 라이브러리가 바로 그 핵심입니다.

위의 종속 컨트랙트를 가져오려면 프로젝트 폴더로 이동하여 `src` 폴더에 새 파일명 `SvgNFT.sol`을 생성하고 상단에 다음 코드를 추가합니다:

solidity

// SPDX-License-Identifier: Unlicense

pragma solidity ^0.8.11;

import "@klaytn/contracts/KIP/token/KIP17/extensions/KIP17URIStorage.sol";

import "@klaytn/contracts/utils/Counters.sol";

import "@klaytn/contracts/utils/Strings.sol";

import "@klaytn/contracts/utils/Base64.sol";

3단계: 스마트 컨트랙트 초기화

컨트랙트 이름을 SvgNFT로 지정하고 종속 컨트랙트(KIP17URIStorage)를 상속하도록 하여 시작해 보겠습니다. SvgNFT.sol의 가져오기 문 아래에 다음 코드를 붙여넣습니다:

solidity 

contract SvgNFT is KIP17URIStorage {

    using Strings for uint256;

    using Counters for Counters.Counter;

    Counters.Counter private _tokenIds;

    event NftMinted(uint _tokenId, string _experience, address _owner);

    event NftUpgraded(uint _tokenId, string _experience, address _owner);

    struct Details {

        uint8 randomFeature;

        uint8 randomCommon;

        uint8 randomEco;

        bool randStatus;

        string features;

        string commonterms;

        string ecosystem;

        string experience;

    }

    mapping(uint => Details) _idToDetails;

    address payable _owner;

}

Let’s quickly go over what each line of code does:

각 코드 줄이 무엇을 하는지 빠르게 살펴보겠습니다:

  • 먼저 StringsCounter 라이브러리를 초기화했습니다. ‘using Strings for uint256`은 “Strings” 라이브러리 내의 모든 메서드를 uint256 유형에 연결한다는 의미입니다. 
  • `using Counters for Counters.Counter`은 current(), increment()와 같은 카운터 라이브러리 내의 모든 함수를 카운터 구조체(Counter struct)에 할당한다는 의미입니다.
  • 다음으로 Counters.Counter 구조체를 _tokenIds라는 개인 변수로 설정합니다. 두 개의 이벤트(NftMintedNftUpgraded)를 선언하고 각각 NFT가 발행되고 업그레이드될 때 발생하도록 했습니다. 
  • 그 후,Details 구조체가 선언되어 NFT에 표시될 동적 디테일(임의의 단어 조합)을 보유하도록 했습니다.
  • 그런 다음 uint(토큰 ID)를 디테일 구조체에 매핑하고 _idToDetails라는 이름을 붙였습니다. 마지막으로 컨트랙트의 소유자인 지불 가능한 owner 주소를 선언했습니다.

4단계: 생성자 초기화

컨트랙트가 모두 제대로 작동하도록 하려면 종속 컨트랙트의 일부 변수를 초기화해야 합니다. 먼저 토큰 이름과 심볼로 KIP17을 초기화해 보겠습니다. 다음으로 _owner 변수를 msg.sender로 설정합니다. 이를 위해 이전 코드 아래에 다음 코드를 붙여넣습니다:

solidity

  constructor() KIP17("SVG NFT", "SVGNFT") {

        _owner = payable(msg.sender);

    }

동적 NFT를 위해 세 개의 무작위 단어를 조합하는 함수를 알아봅시다. 

이를 위해 각 단어 테마에 대해 고정 배열을 만들 것입니다. 테마는 다음과 같습니다:

  • Unique feature: Klaytn의 고유 기능 목록
  • Ecosystem: Klaytn의 생태계 파트너 목록
  • Common Web3 terms: 일반적인 web3 용어 목록

각 배열은 이렇게 생겼습니다:

solidity

string[10] memory features = [

            "1-second block generation",

            "EVM Compatibility",

            "Fee delegation",

            "4000 transaction per seconds",

            "Low gas price ",

            "Scaling Solutions - Service chains",

            "Ethereum Compatibility",

            "Open Source Infrastructure",

            "Protocol-level Eco Fund",

            "Governance by DAOs"

        ];

string[10] memory eco = [

            "Safepal",

            "Tatum",

            "Orakl Network",

            "Synapse",

            "Ozys",

            "Rabby Wallet",

            "Witnet",

            "ZKrypyo",

            "Klap Protocol",

            "Swapscanner"

        ];

string[10] memory common = [

            "ECDSA",

            "Metamask",

            "EOA",

            "Account Abstraction",

            "Gas Fees",

            "Cold Wallet",

            "NFTs",

            "KIP17",

            "Private Key",

            "IDE"

        ];

이제 배열에서 임의의 단어를 반환하는 함수를 만들어야 합니다. 이 함수는 각 단어 테마(고유 기능, 공통 용어, 생태계)에 대해 선언한 다음 변수 이름이 randomSeed인 의사 난수 생성기를 사용하여 임의의 단어를 저장합니다. 

난수값이 저장되지 않은 경우 난수를 생성하여 난수 단어를 저장하고, 그렇지 않은 경우 이미 생성된 난수값을 반환합니다. 이렇게 하려면 이전 코드 아래에 코드를 붙여넣습니다.

solidity

    //stores a random unique feature word 

    function _setUniqueFeatures(uint _tokenId) private {

        string[11] memory features = [

"",

            "1-second block generation",

            "EVM Compatibility",

            "Fee delegation",

            "4000 transaction per seconds",

            "Low gas price ",

            "Scaling Solutions - Service chains",

            "Ethereum Compatibility",

            "Open Source Infrastructure",

            "Protocol-level Eco Fund",

            "Governance by DAOs"

        ];

        uint randomSeed = uint(

            keccak256(abi.encodePacked(tx.origin, block.timestamp))

        );

        if (_idToDetails[_tokenId].randomFeature == 0) {

            uint8 random = uint8(randomSeed % features.length -1) + 1;

            _idToDetails[_tokenId].features = features[random];

            _idToDetails[_tokenId].randomFeature = random;

        }

        _idToDetails[_tokenId].features = features[

            _idToDetails[_tokenId].randomFeature

        ];

    }

    // stores a random common web3 term

    function _setCommonTerms(uint _tokenId) private {

        string[11] memory common = [

"",

            "ECDSA",

            "Metamask",

            "EOA",

            "Account Abstraction",

            "Gas Fees",

            "Cold Wallet",

            "NFTs",

            "KIP17",

            "Private Key",

            "IDE"

        ];

        uint randomSeed = uint(

            keccak256(abi.encodePacked(tx.origin, block.timestamp))

        );

        if (_idToDetails[_tokenId].randomCommon == 0) {

            uint8 random = uint8(randomSeed % common.length - 1) + 1;

            _idToDetails[_tokenId].commonterms = common[random];

            _idToDetails[_tokenId].randomCommon = random;

        }

        _idToDetails[_tokenId].commonterms = common[

            _idToDetails[_tokenId].randomCommon

        ];

    }

    // stores random ecosystem word 

    function _setEcosystem(uint _tokenId) private {

        string[11] memory eco = [

"",

            "Safepal",

            "Tatum",

            "Orakl Network",

            "Synapse",

            "Ozys",

            "Rabby Wallet",

            "Witnet",

            "ZKrypyo",

            "Klap Protocol",

            "Swapscanner"

        ];

        uint randomSeed = uint(

            keccak256(abi.encodePacked(tx.origin, block.timestamp))

        );

        if (_idToDetails[_tokenId].randomEco == 0) {

            uint8 random = uint8(randomSeed % eco.length -1) + 1;

            _idToDetails[_tokenId].ecosystem = eco[random];

            _idToDetails[_tokenId].randomEco = random;

        }

        _idToDetails[_tokenId].ecosystem = eco[

            _idToDetails[_tokenId].randomEco

        ];

    }

5단계: SVG 이미지 생성을 위한 generateSVGImage 함수 생성하기

앞서 언급했듯이, 저희는 컨트랙트로 가져온 base64 라이브러리를 사용하여 온체인에 직접 파일을 저장하기 위해 데이터를 데이터 URI 사양으로 변환할 것입니다. 아래 코드는 세 개의 임의의 단어 조합으로 SVG를 생성합니다. 이전 코드 아래에 다음을 붙여넣습니다:

solidity 

function generateSVGImage(

        string memory _experience,

        uint tokenId

    ) public returns (string memory) {

        _setUniqueFeatures(tokenId);

        _setCommonTerms(tokenId);

        _setEcosystem(tokenId);

        bytes memory svg = abi.encodePacked(

            '<svg xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMinYMin meet" viewBox="0 0 350 350">',

            "<style>.base { fill: black; font-family: serif; font-size: 14px;}</style>",

            '<rect width="100%" height="100%" fill="white" ry="40" rx="40" style="stroke:#FF2F00; stroke-width:5; opacity:0.9" />',

            '<text x="50%" y="20%" class="base" dominant-baseline="middle" text-anchor="middle">',

            "Klaytn",

            "</text>",

            '<text x="50%" y="30%" class="base" dominant-baseline="middle" text-anchor="middle">',

            _idToDetails[tokenId].features,

            "</text>",

            '<text x="50%" y="40%" class="base" dominant-baseline="middle" text-anchor="middle">',

            _idToDetails[tokenId].commonterms,

            "</text>",

            '<text x="50%" y="50%" class="base" dominant-baseline="middle" text-anchor="middle">',

            _idToDetails[tokenId].ecosystem,

            "</text>",

            '<text x="50%" y="60%" class="base" dominant-baseline="middle" text-anchor="middle">',

            _experience,

            "</text>",

            '<text x="1.2%" y="90%" class="base" dominant-baseline="middle" text-anchor="left" style="font-size:10px;"> ',

            " Klaytn is an open source public blockchain designed for tommorrow's on-chain world ",

            "</text>",

            "</svg>"

        );

        return

            string(

                abi.encodePacked(

                    "data:image/svg+xml;base64,",

                    Base64.encode(svg)

                )

            );

    }

위의 코드에서 함수가 _experiencetokenId를 매개변수로 받는 것을 볼 수 있습니다. 

다음으로 바이트 타입의 svg 변수를 선언했습니다. 이 변수는 abi.encodePacked() 함수를 사용하여 바이트 배열로 변환된 NFT의 이미지를 나타내는 SVG 코드를 저장합니다. 

동일한 SVG 코드에서 임의의 단어 조합을 나타내는 몇 가지 변수(_idToDetails[tokenId].features, _idToDetails[tokenId].commonterms, _idToDetails[tokenId].ecosystem)와 사용자가 경험 수준을 업그레이드할 때 변경되는 _experience를 전달한 것을 볼 수 있습니다. 

SVG는 XML로 작성되므로 변수를 전달하여 특정 상호 작용에 따라 SVG를 동적으로 변경할 수 있습니다.

마지막으로, 이 함수는 abi.encodePacked() 함수를 사용하여 데이터 URI 사양을 앞에 붙인 Base64.encode()를 사용하여 Base64로 인코딩된 버전의 SVG를 반환합니다. 

이 반환값을 브라우저에 붙여넣으면 지정된 임의의 단어가 포함된 SVG 이미지가 열립니다.

6단계: tokenURI를 생성하는 generateTokenURI 함수 생성하기

이 경우 tokenURI는 NFT 메타데이터(이미지 및 NFT의 기타 속성)를 가리킵니다. 이를 온체인에 직접 저장하려면, 위 단계의 SVG 이미지에서 했던 것처럼 인코딩해야 합니다. 이 코드를 이전 코드 아래에 붙여넣습니다: 

참고: 이 함수는 데이터 URI 사양에 따라 토큰 URI를 생성하기 위한 구현으로, IPFS를 사용하여 오프체인에 저장하는 것보다 NFT 세부 정보를 온체인에 저장하는 데 적합합니다.

solidity

    function generateTokenURI(

        uint256 tokenId,

        string memory _experience

    ) public returns (string memory) {

        bytes memory dataURI = abi.encodePacked(

            "{",

            '"name": "SVG NFT #',

            tokenId.toString(),

            '",',

            '"description": "SVG NFT on Klaytn",',

            '"image": "',

            generateSVGImage(_experience, tokenId),

            '"',

            "}"

        );

        return

            string(

                abi.encodePacked(

                    "data:application/json;base64,",

                    Base64.encode(dataURI)

                )

            );

    }

각 코드 줄이 무엇을 하는지 빠르게 살펴보겠습니다:

  • 바이트 타입의 dataURI 변수는 abi.encodePacked 함수를 사용하여 NFT에 대한 메타데이터 객체를 생성합니다. 
  • JSON 객체에서 “name” 프로퍼티는 “SVG NFT #” 및 tokenId.toString() 값을 취하고, “image” 프로퍼티는 generateSVGCard() 함수에서 생성된 값을 취하는 것을 알 수 있습니다.
  • 마지막으로, 이 함수는 SVG 이미지에서 수행한 것과 마찬가지로 JSON 메타데이터와 함께 Base64로 인코딩된 데이터URI 버전을 나타내는 바이트 배열을 포함하는 문자열을 반환합니다. 이 반환값을 브라우저에 붙여넣으면 메타데이터 사양이 포함된 JSON 객체가 열립니다.

7단계: 민트 함수 생성하기

민트 함수는 온체인 메타데이터가 포함된 NFT를 생성합니다. 이 함수를 생성하려면 이전 코드 아래에 이 코드를 붙여넣습니다:

solidity

    function mint() public {

        _tokenIds.increment();

        uint256 newItemId = _tokenIds.current();

        _safeMint(msg.sender, newItemId);

        _idToDetails[newItemId].experience = "Beginner";

        _setTokenURI(

            newItemId,

            generateTokenURI(newItemId, _idToDetails[newItemId].experience)

        );

        emit NftMinted(

            newItemId,

            _idToDetails[newItemId].experience,

            msg.sender

        );

    }

항상 그렇듯이 각 코드 줄이 하는 일을 빠르게 살펴보겠습니다:

먼저 _tokenIds 변수의 값을 증가시킨 다음, 현재 값을 newItemId라는 새로운 uint256 변수에 저장합니다.

그런 다음 KIP17 컨트랙트에서 _safeMint 함수를 호출합니다. 이 함수는 msg.sendernewItemId를 인수로 받습니다. 그런 다음 NFT의 경험 수준을 “Beginner”로 설정합니다. 

그 후, 새로운 아이템아이디와 getTokenURI()의 반환값을 전달하는 토큰 URI를 설정합니다.

이렇게 하면 NFT 메타데이터를 온체인에 완전히 저장할 수 있으며 스마트 콘트랙트에서 직접 메타데이터를 업데이트할 수 있습니다.

8단계: upgradeNFTLevel 함수 만들기

이 함수는 사용자가 초급에서 전문가로 레벨을 업그레이드할 때 NFT의 온체인 메타데이터를 업데이트하는 역할을 담당합니다. 계속하려면 이전 코드 아래에 이 코드를 붙여넣으세요:

solidity

    function upgradeNFTLevel(uint256 tokenId) public payable {

        require(_exists(tokenId), "Please use an existing token");

        require(

            ownerOf(tokenId) == msg.sender,

            "You must own this token to train it"

        );

        require(

            msg.value == 10 ether || msg.value == 30 ether,

            "Send exact upgrade fee"

        );

        if (msg.value == 10 ether) {

            _idToDetails[tokenId].experience = "Intermediate";

        } else if (msg.value == 30 ether) {

            _idToDetails[tokenId].experience = "Expert";

        } 

        string memory experience =  _idToDetails[tokenId].experience;

        _setTokenURI(tokenId, generateTokenURI(tokenId, experience));

        emit NftUpgraded(tokenId, _idToDetails[tokenId].experience, msg.sender);

    }

위의 코드에서 함수가 토큰아이디라는 하나의 매개변수를 받는 것을 확인할 수 있습니다. 다음으로 3개의 require 문을 설정하여 확인합니다:

  • 토큰은 KIP17의 _exists() 함수를 사용하여 존재합니다.
  • msg.sender가 NFT tokenId의 소유자입니다.
  • intermediate(10 KLAY) 및 expert(30 KLAY)에 대한 업그레이드 수수료가 지불되었습니다.

이후 전송된 값을 기반으로 각 토큰아이디에 대한 NFT의 레벨과 업그레이드 수수료를 설정하는 조건을 초기화했습니다. 지불한 수수료에 따라 NFT의 레벨을 설정하고, 그렇지 않으면 초급으로 설정합니다. 

또한 _idToDetails[tokenid].experience 매핑에 저장된 값을 사용하여 NFT의 experience 레벨을 얻었습니다.

다음으로 _setTokenURI 함수를 호출하여 토큰아이디와 getTokenURI(tokenId, experience)의 반환값을 전달했습니다. 이 함수는 새로운 경험 레벨을 포함하도록 온체인 메타데이터를 변경합니다.

마침내 우리는  NftUpgraded  이벤트를 발생시켰습니다. 

9단계: 기타 기능

A: withdraw(): 출금 기능을 통해 소유자는 이 컨트랙트에서 사용 가능한 KLAY를 출금할 수 있습니다.

solidity

    function withdraw() public {

        require(_owner == msg.sender, "Not owner");

        require(_owner.send(address(this).balance));

    }

B: getNftExperience(): 이 보기 함수는 토큰 ID가 주어진 NFT의 경험치 레벨을 반환합니다.

solidity

    function getNftExperience(

        uint tokenId

    ) public view returns (string memory) {

        string memory experience = _idToDetails[tokenId].experience;

        return experience;

    }

이제 동적 SVG NFT 스마트 컨트랙트 작성을 완료했으니 다음 단계는 Klaytn Testnet Baobab에 스마트 컨트랙트를 배포하고 파운드리를 사용해 상호작용하는 것입니다.

Foundry를 사용해 동적 SVG NFT 스마트 컨트랙트 배포하기

이 섹션에서는 빠른 스마트 컨트랙트 개발 툴체인인 Foundry를 사용하여 동적 SVG NFT 스마트 컨트랙트를 컴파일, 테스트하고 Klaytn 테스트넷 Baobab에 배포하는 방법을 설명합니다. 

참고: 파운드리 프로젝트 루트 폴더에 위치해야 합니다.

컨트랙트를 배포하기 위해 다음 단계를 진행합니다:

1단계: 컨트랙트 컴파일

컨트랙트를 컴파일하려면 터미널에서 아래 코드를 실행합니다:

bash

forge build 

이 명령을 성공적으로 실행하면 터미널이 다음과 같이 표시되어야 합니다:

bash

[⠆] Compiling...

[⠘] Compiling .. files with 0.8.19

[⠃] Solc 0.8.19 finished in 8.77s

2단계: 컨트랙트 테스트

컨트랙트를 테스트하려면  test  폴더로 이동하여 `Svgnft.t.sol`이라는 새 파일을 생성합니다. 테스트 파일에 아래 코드를 붙여넣습니다.

solidity

// SPDX-License-Identifier: UNLICENSED

pragma solidity ^0.8.13;

import "forge-std/Test.sol";

import "../src/SvgNFT.sol";

contract SvgNftTest is Test {

    SvgNFT public svgnft;

    // set a specified address for the prank action

    address _prankAddr = 0x2330384317C95372875AD81d5D6143E7735714A5;

    // an optional function invoked before each test case is run.

    function setUp() public {

        svgnft = new SvgNFT();

        // sets msg.sender to the specified address for the next call.

        vm.prank(_prankAddr);

        svgnft.mint();

    }

    function testCheckMint() public {

        assertEq(svgnft.balanceOf(_prankAddr), 1);

    }

    function testFail_UpgradeFee_lessThanZero() public {

       // sets up a prank from an address that has some ether.

        hoax(_prankAddr, 2 ether);

        svgnft.upgradeNFTLevel{ value: 1 ether }(1);

    }

    function testSvgNftwithUpgradeFeeOfTen() public payable {

        hoax(_prankAddr, 12 ether);

        svgnft.upgradeNFTLevel{ value : 10 ether }(1);

    }

    function testSvgNftwithUpgradeFeeOfThirty() public {

        hoax(_prankAddr, 32 ether);

        svgnft.upgradeNFTLevel{ value: 30 ether }(1);

    }

}

위의 코드는 다음의 항목을 테스트합니다:

  • 동적 NFT가 지정된 주소로 발행되었는지 여부;
  • 해당 업그레이드 수수료가 더 낮게 지불되었을 때 uprageNFTLevel 함수가 실패하는 경우. 

파운드리 테스트에 대한 자세한 내용은 여기에서 확인할 수 있습니다. 

위의 테스트를 실행하려면 아래 코드를 실행하세요:

bash

Forge test 

명령을 실행한 후 터미널의 모습은 다음과 같습니다:

bash

[PASS] testCheckMint() (gas: 10068)

[PASS] testFail_UpgradeFee_lessThanZero() (gas: 20639)

[PASS] testSvgNftwithUpgradeFeeOfTen() (gas: 470220)

[PASS] testSvgNftwithUpgradeFeeOfThirty() (gas: 463891)

Test result: ok. 4 passed; 0 failed; finished in 6.32ms

3단계: 컨트랙트 배포

동적 svg 컨트랙트를 Klaytn 테스트넷 Baobab에 배포하려면 아래 명령어를 실행합니다:

bash

// format

forge create --rpc-url $RPC_URL --private-key $PRIVATE_KEY src/Svgnft.sol:SvgNFT
bash

// change the rpc and private key here

forge create --rpc-url https://public-en-baobab.klaytn.net --private-key 0xaaabbbccc… src/Svgnft.sol:SvgNFT

이 명령을 성공적으로 실행하면 다음과 같은 출력이 표시됩니다:

bash

[⠆] Compiling...

No files changed, compilation skipped

Deployer: 0x2330384317C95372875AD81d5D6143E7735714A5

Deployed to: 0x620715ddFBc401857FcF4970919A668219a34f4b

Transaction hash: 0xe455e90a66b8f0ee0129ca5516ddea59b70db92ce1aaf3003034a6ef8b3e2571

새로 생성된 주소를 Klaytnscope에 붙여넣어 트랜잭션을 확인할 수 있습니다. 

드디어 여러분은 메타데이터가 온체인에 완전히 포함된 동적 NFT 콘트랙트를 배포했습니다. 

다음 단계는 NFT를 발행하고 업그레이드하여 컨트랙트와 상호작용하는 것입니다. 

동적 SVG 스마트 콘트랙트와 상호작용하기

이 섹션에서는 새로운 동적 NFT를 발행하고 upgradeNFTLevel 함수를 호출하여 온체인에서 메타데이터가 변경되는 것을 확인합니다. 다음 단계를 따라해 보겠습니다:

1. NFT 민팅하기

NFT를 민팅하려면 프로젝트 폴더 터미널을 열고 아래 명령을 실행합니다:

bash

// format

cast send --rpc-url=<RPC-URL> <CONTRACT-ADDRESS> "function(uint256)" arg --private-key=<PRIVATE-KEY>
bash

// change the rpc and private key here

cast send --rpc-url=https://public-en-baobab.klaytn.net 0x620715ddFBc401857FcF4970919A668219a34f4b "mint()" --private-key=0xaaabbbccc…

이 명령을 성공적으로 실행하면 터미널이 다음과 같이 표시되어야 합니다:

방금 첫 번째 동적 NFT를 발행했습니다! 확인하려면 OpenSea로 이동하여 새로 발행된 NFT를 확인하세요.

2. NFT 업그레이드하기

NFT 체험 레벨을 업그레이드하려면 터미널에서 아래 코드를 실행하세요:

참고: 중급은 10 KLAY, 전문가는 30 KLAY의 업그레이드 수수료를 지불해야 합니다.
bash

// format

cast send --rpc-url=<RPC-URL> <CONTRACT-ADDRESS> "function(uint256)" arg --private-key=<PRIVATE-KEY> —value 10ether
bash

// change the rpc,  private key and value 

cast send --rpc-url=https://public-en-baobab.klaytn.net 0x620715ddFBc401857FcF4970919A668219a34f4b "upgradeNFTLevel

(uint256)" 1 --private-key=0xaaabbbccc… --value 10ether

NFT를 성공적으로 업그레이드한 후 OpenSea로 이동하여 NFT의 경험치가 중급으로 변경되었는지 확인합니다.

참고: 이 업데이트를 확인하려면 OpenSea에서 메타데이터를 새로고침하고 페이지를 다시 로드해야 합니다. OpenSea에서 메타데이터를 업데이트하는 데 몇 분 정도 걸릴 수 있습니다. 업데이트하는 동안 커피 한 잔 마셔보세요!

여러분은 방금 스마트 콘트랙트 함수를 호출하여 NFT의 온체인 메타데이터를 업그레이드 했습니다. Voila!

마치며

이번 튜토리얼을 통해 동적 NFT가 무엇인지, 동적 SVG NFT를 작성하는 방법, 파운드리를 사용해 스마트 컨트랙트를 배포하고 상호작용하는 방법을 배웠습니다. 이 튜토리얼은 Klaytn Testnet Baobab에 배포되었으며, 동일한 코드가 Klaytn Mainnet Cypress에서도 작동합니다. 

이 튜토리얼에서는 의사 난수 생성기를 무작위성 소스로 사용했는데, 이는 최상의 무작위성 소스는 아닙니다. 다음 단계로 스마트 컨트랙트에서 검증 가능한 난수 생성을 위해 Orakl VRF를 사용하는 방법을 살펴볼 수 있습니다. 
파운드리를 사용해 클레이튼을 구축하는 방법에 대한 자세한 내용은 Klaytn Docs Foundry Docs