[0x02] Mint my own NFT collection + Make a Web3 App

c0np4nn4·2022년 2월 14일
1

Buildspace

목록 보기
2/2
post-thumbnail

[0x00] OpenSea에서 NFT Mint하기


0x00: NFT?

  • NFT는 특정한 데이터와 연결지을 수 있는 하나의 토큰이다.
  • 재밌는 점은, 이 토큰유일무이하다는 것이다.
  • 예시를 살펴보며 NFT에 대해 좀 더 자세히 이해해보도록 한다.

0x01: 피카소 스케치

(그림 출처)

  • 예를 들어, 피카소가 스케치 그림 100장을 판매한다고 해보자.
  • 피카소는 연필과 종이를 이용해 스케치 그림 100장을 그릴 것이다.
  • 피카소의 작품은 각각 유일무이하다.
    • 눈으로 봤을 때 서로 다름이 구별되기 때문이다.
  • 피카소는 갤러리를 통해 스케치 그림들을 판매할 수 있다.
  • 구매자는 이 스케치 그림을 피카소가 그렸다고 확신하는데 그 이유는 다음과 같다.
    • 피카소 자신이 스케치 그림을 그렸다고 주장하기 때문이다.
    • 피카소의 서명이 그림에 있기 때문이다.
    • 스케치 그림을 판매한 갤러리에서 ""피카소 오리지널""임을 증명하는 증명서를 주었기 때문이다.
  • 구매자는 이 증명서가 장기적으로 유효할 것이라 믿어야 하지만, 이는 매우 간단하다. 그냥 믿으면 되기 때문이다.

0x02: 피카소 NFT

  • 이제 이 예를 NFT 영역에서 생각해보자.
  • 피카소가 JPEG 파일같은 디지털 그림을 100장 그린 뒤 판매한다고 가정한다.
  • 피카소는 스마트 컨트랙트를 작성하여 자신의 NFT Collection을 만들 것이다.
  • 피카소는 각각의 작품에 유일무이한 구별자를 부여할 것이다. (Sketch#1, Sketch#2, Sketch#3, 등등)
  • 피카소가 NFT Collection출시 할 준비가 되었을 때, 그는 자신의 이더리움 지갑을 이용해서 NFT Collection서명한다.
  • 그 후, 피카소가 NFT를 판매하는 것은 단순히 NFT를 구매자의 지갑 주소로 transfer하는 과정이다.
  • 구매자는 피카소의 일종의 피카소 오리지널을 피카소 본인으로부터 받게 된다.
  • 구매자는 아래의 방법들로 이 데이터가 피카소 오리지널임을 증명할 수 있다.
    • 피카소가 서명하고 작성스마트 컨트랙트로부터 NFT를 전송 받았기 때문에 이는 피카소 오리지널이다.
    • 피카소가 작품을 만들 때 부여했던 유일무이한 구별자를 통해 이것이 오리지널임을 확인 할 수 있다.
  • 이를 이용하면 어떠한 형식의 객체라도 유일무이하고 가치 있게만들 수 있다.
  • 실제로 NFT예술 작품, 게임 등이 연관된 프로젝트가 아주 많이 진행되고 있다.

0x03: 환경 셋업

<로컬 이더리움 네트워크>

  • 로컬 이더리움 네트워크를 통해 실습을 진행한다.
  • 스마트 컨트랙트컴파일되고 호출되는 것을 로컬 상에서 실습할 수 있다.
  • 본 튜토리얼의 전체적인 로드맵은 아래와 같다.
  • 스마트 컨트랙트를 작성한다.
  • 작성한 스마트 컨트랙트블록체인 상에 배포(deploy)한다.
  • 웹사이트를 만들어, 누구나 쉽게 우리의 NFT CollectionMint 할 수 있도록 한다.
  • 이더리움이 어떻게 동작하는지를 이해하는데 좋은 자료 ( 링크 )

0x04: Hardhat

  • hardhat 이라는 을 이용하여 실습을 진행한다.
  • Nodenpm을 이용해 설치할 수 있다.
  • 아래의 명령어를 입력해 환경을 세팅해두도록 하자.
mkdir epic-nfts
cd epic-nfts
npm init -y
npm install --save-dev hardhat
  • hardhat에 대한 보다 자세한 설명은 ( [0x01] ) 또는 ( Hardhat Tutorial ) 두 곳을 참조하면 좋을 것 같다.

0x05: 샘플 프로젝트

  • hardhat의 설치를 마쳤다면, 샘플 프로젝트를 만들 수 있다.
  • 아래와 같이 입력한다.
    npx hardhat
  • 첫 번째 선택지인 Create a basic sample project를 선택한 후 전부 Enter키로 넘기면 아래와 같이 설치되는 과정이 나온다.
  • 이후 아래 명령을 입력하여 hardhat-wafflehardhat-ether를 추가로 설치해주어야 한다.
    npm install --save-dev @nomiclabs/hardhat-waffle ethereum-waffle chai @nomiclabs/hardhat-ethers ethers
  • 또한, OpenZeppelin이라는 라이브러리를 이용할 것이다. 아래와 같이 입력하여 추가로 설치한다.
    npm install @openzeppelin/contracts
  • 설치가 모두 완료되었으면, 아래와 명령을 입력하여 동작하는지 확인한다.
    npx hardhat run scripts/sample-script.js
  • 여기서 한 작업을 정리하면 아래와 같다.
    1. Hardhat으로 스마트 컨트랙트컴파일하여 바이트코드를 생성한다.
    2. Hardhat은 일회성의 로컬 블록체인을 돌린다. 일회성이란, 스마트 컨트랙트의 실행이 끝나면 종료됨을 의미한다.
    3. Hardhat스마트 컨트랙트로컬 블록체인 네트워크배포(deploy)한다.

0x06: 스마트 컨트랙트 작성

  • ( [0x01] ) 를 끝내고 왔다면, 이후 부분은 동일하기 때문에 빠르게 넘어가도 된다.
  • 이제 본격적으로 스마트 컨트랙트를 작성한다.
  • 이번에 구현할 컨트랙트는 아래와 같다.
    // SPDX-License-Identifier: UNLICENSED
    
    pragma solidity ^0.8.4;
    
    import "hardhat/console.sol";
    
    contract MyEpicNFT {
       constructor() {
            console.log("This is my NFT contract. Whoa!");
        }
    }
  • 각 코드 라인에 대한 설명은 ( 이곳 ) 을 참조하면 된다.

0x07: 컨트랙트 실행 스크립트(run.js) 작성

  • 컨트랙트를 작성한 뒤, 다음 세 작업을 행해야 한다.
    1. Hardhat으로 스마트 컨트랙트컴파일하여 바이트코드를 생성한다.
    2. Hardhat은 일회성의 로컬 블록체인을 돌린다. 일회성이란, 스마트 컨트랙트의 실행이 끝나면 종료됨을 의미한다.
    3. Hardhat스마트 컨트랙트로컬 블록체인 네트워크배포(deploy)한다.
  • 이 때, JavaScript를 이용하여 스크립트를 작성하면 보다 간단히 작업을 할 수 있다.
  • 아래의 run.js파일을 scripts/디렉토리 밑에 작성한다.
    const main = async () => {
      const nftContractFactory = await hre.ethers.getContractFactory('MyEpicNFT');
      const nftContract = await nftContractFactory.deploy();
      await nftContract.deployed();
      console.log("Contract deployed to:", nftContract.address);
    };
    
    const runMain = async () => {
      try {
        await main();
        process.exit(0);
      } catch (error) {
        console.log(error);
        process.exit(1);
      }
    };
    
    runMain();

0x08: 스크립트(run.js) 분석

const nftContractFactory = await hre.ethers.getContractFactory('MyEpicNFT');
  • MyEpicNft라는 이름의 컨트랙트를 참조한다.
const nftContract = await nftContractFactory.deploy();
  • 스크립트 실행 후 일회성 로컬 블록체인 네트워크를 만들었다가 스크립트가 끝나면 같이 네트워크를 파괴한다.
await nftContract.deployed();
  • 컨트랙트로컬 블록체인 네트워크에 실제로 배포되기 전까지 기다린다.
console.log("Contract deployed to:", nftContract.address);
  • 한 번 배포되고 나면 해당 컨트랙트의 주소를 console log를 이용해서 출력한다.

0x09: 스크립트(run.js) 실행!

  • 아래 명령어를 입력하여 스크립트를 실행한다.
npx hardhat ./scripts/run.js
  • 코드 내에서 자주 보이는 hre 에 대한 이해를 위해 ( 이 곳 ) 을 참조해도 좋다.

0x0A: NFT Minting 컨트랙트

  • 앞서 작성한 MyEpicNFT.sol을 아래와 같이 수정한다.
pragma solidity ^0.8.4;

// We first import some OpenZeppelin Contracts.
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
import "hardhat/console.sol";

// We inherit the contract we imported. This means we'll have access
// to the inherited contract's methods.
contract MyEpicNFT is ERC721URIStorage {
  // Magic given to us by OpenZeppelin to help us keep track of tokenIds.
  using Counters for Counters.Counter;
  Counters.Counter private _tokenIds;

  // We need to pass the name of our NFTs token and its symbol.
  constructor() ERC721 ("SquareNFT", "SQUARE") {
    console.log("This is my NFT contract. Woah!");
  }

  // A function our user will hit to get their NFT.
  function makeAnEpicNFT() public {
     // Get the current tokenId, this starts at 0.
    uint256 newItemId = _tokenIds.current();

     // Actually mint the NFT to the sender using msg.sender.
    _safeMint(msg.sender, newItemId);

    // Set the NFTs data.
    _setTokenURI(newItemId, "blah");

    // Increment the counter for when the next NFT is minted.
    _tokenIds.increment();
  }
}
  • 새로운 코드에서는 상속(inheretance)이라는 개념을 사용하고 있다. ( 참고 )
  • NFTERC721을 스탠다드로 한다. ( 참고 )
  • OpenZeppelin은 이 NFT의 스탠다드를 구현해 놓은 라이브러리이며, 이를 이용해 우리 마음껏 NFT를 커스터마이즈 하는 것이 가능하다.
  • ERC721에 관한 OpenZeppelin의 상세는 ( 이 곳 )에서 확인 할 수 있다.
  • 코드를 하나 하나 살펴보자.
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
import "hardhat/console.sol";
  • 우선 OpenZeppelin컨트랙트들을 불러온다.
contract MyEpicNFT is ERC721URIStorage { ... }
  • ERC721URIStorage 라는 컨트랙트를 상속 받고 있다.
  • 이제 MyEpicNFTERC721URIStorage의 함수 등을 쓸 수 있다.
using Counters for Counters.Counter;
Counters.Counter private _tokenIds;
  • OpenZeppelin에서 가져온 Counters를 통해 tokenID를 보다 쉽게 트래킹 할 수 있다.
constructor() ERC721 ("SquareNFT", "SQUARE") {
    console.log("This is my NFT contract. Woah!");
}
  • ERC721의 두 인자로는 이름(name)심볼(symbol)을 전달해야 한다.
function makeAnEpicNFT() public {
    uint256 newItemId = _tokenIds.current;
    _safeMint(msg.sender, newItemId);
    _setTokenURI(newItemId, "blah");
    _tokenIds.increment();
  }
}
  • makeAnEpicNFT() 함수를 유저들이 호출하면 NFT를 받을 수 있다.
  • newItemId에 현재 토큰 아이디를 넣는다. 초기값은 0이다.
  • _safeMint()에서는 newItemIdid로 하여 msg.sender에게 NFTMint한다.
    • msg.sender를 사용하면 조금 더 안전한 코딩을 할 수 있다.
  • _setTokenURI()에서는 newItemIdid로 하는 NFT에 두 번쨰 인자를 데이터로 설정한다.
  • _tokenIds.increment()를 통해 카운트 값을 증가시킨다.

0x0B: tokenURI와 로컬에서 실행해보기

  • tokenURINFT의 실제 데이터가 있는 곳을 말한다.
  • 보통은 metadata라고 불리는 JSON파일의 링크를 의미한다.
  • 아래는 metadata의 예시이다.
    {
    	"name": "Spongebob Cowboy Pants",
    	"description": "A silent hero. A watchful protector.",
    	"image": "https://i.imgur.com/v7U019j.png"
    }
  • 이 형식을 커스터마이즈 하는 것은 가능하지만, 보통은 structure를 맞춰주는 것이 좋다.
  • 만약 structure를 무시하고 작업할 경우, OpenSea와 같은 NFT관련 서비스를 이용하는 데 문제가 발생할 수 있다.
  • ERC721metatdata 스탠다드는 이 세 가지 이며, 이를 따름으로써 보다 효과적으로 NFT를 활용할 수 있다.
  • ( JSONkeeper website ) 를 통해서, 위의 ERC721 metadata에 대한 JSON 링크를 생성할 수 있다.
  • 나는 아래와 같이 조금 수정하여 튜토리얼을 진행하였다.
  • 아래 이미지를 NFT 화 해보도록 한다.
    {
    	"name": "Epic Smile",
    	"description": "Look at this big smile.",
    	"image": "https://images.velog.io/images/c0np4nn4/post/39bc50e1-674e-4472-a7a7-1a40c3f59eb0/1.PNG"
    }
  • 내 그림의 metadata 링크는 https://jsonkeeper.com/b/UZ1G 이다.
  • 이제 컨트랙트로 돌아가서 _setTokenURI() 함수를 수정한다.

    _setTokenURI(newItemId, "https://jsonkeeper.com/b/UZ1G");
  • 또한 바로 아래 라인에 다음과 같은 코드를 추가하여 Mint가 되었는지 체크한다.

    console.log("An NFT w/ ID %s has been minted to %s", newItemId, msg.sender);
  • 전체 컨트랙트코드를 다시 한 번 정리하면 아래와 같다.
pragma solidity ^0.8.4;

import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
import "hardhat/console.sol";

contract MyEpicNFT is ERC721URIStorage {
  using Counters for Counters.Counter;
  Counters.Counter private _tokenIds;

  constructor() ERC721 ("SquareNFT", "SQUARE") {
    console.log("This is my NFT contract. Woah!");
  }

  function makeAnEpicNFT() public {
    uint256 newItemId = _tokenIds.current();

    _safeMint(msg.sender, newItemId);

    // Updated!
    _setTokenURI(newItemId, "https://jsonkeeper.com/b/UZ1G");
    console.log("An NFT w/ ID %s has been minted to %s", newItemId, msg.sender);

    _tokenIds.increment();
  }
}

0x0C: 로컬에서 Mint하기

  • 이제 run.js파일을 수정해서 makeAnEpicNFT()함수를 호출하기만 하면 된다.
  • 수정한 run.js파일은 아래와 같다.
const main = async () => {
  const nftContractFactory = await hre.ethers.getContractFactory('MyEpicNFT');
  const nftContract = await nftContractFactory.deploy();
  await nftContract.deployed();
  console.log("Contract deployed to:", nftContract.address);

  // Call the function.
  let txn = await nftContract.makeAnEpicNFT()
  // Wait for it to be mined.
  await txn.wait()

  // Mint another NFT for fun.
  txn = await nftContract.makeAnEpicNFT()
  // Wait for it to be mined.
  await txn.wait()

};

const runMain = async () => {
  try {
    await main();
    process.exit(0);
  } catch (error) {
    console.log(error);
    process.exit(1);
  }
};

runMain();
  • 코드에 적힌 주석대로 두 번 NFTMint하여, 아래 명령어를 입력한 뒤 결과를 확인하면 아래와 같다.
npx hardhat run ./scripts/run.js
  • 같은 이미지에 대한 metadata_setTokenURI()를 호출하고 있으므로, newItemId는 둘 다 동일한 값임을 확인할 수 있다.
  • 이후에 이 값을 계속 바꿔나가는 것도 코딩할 예정이다.

<디버깅>

  • 사실은 실행 과정에서 에러가 좀 났었는데, 디버깅 한 과정을 정리해보려고 한다.
  • 처음 난 에러는 아래와 같다.
  • current()가 없을리는 만무하지만 한 번 직접 보고 싶었다.
  • 함수는 진짜 있는데... 그럼 어디가 문제일까? 하고 튜토리얼의 코드와 내 코드를 비교해봤다.
  • 그 결과, 아래의 한 줄을 빼먹은 것이 발견되었다.
    using Counters for Counters.Counter;
  • Solidity에서 using A for B 구문은 A라이브러리의 함수B에 붙이는 구문이다.
  • 마치 python에서의 함수가 첫 번째 인자를 self로 두는 것처럼, A라이브러리의 함수B를 첫 번째 인자로 생각할 수 있다.
  • ( 참고 사이트 )
  • 우리의 코드를 다시 보며 조금 쉽게 설명해보고자 한다.
    contract MyEpicNFT is ERC721URIStorage {
    	using Counters for Counters.Counter;
    	Counters.Counter private _tokenIds;
        /* ... */
        /* ... */
        _tokenIds.increment();
    } ```
  • 우선 Counters.Counter를 자료형으로 하는 변수들이 Counters의 함수를 사용할 수 있도록 선언한다.
  • Counters.Counter를 자료형으로 하는 private변수 _tokenIds를 선언한다.
  • 일련의 작업 후, _tokenIds는 자료형이 Counters.Counter이므로, Counters의 함수 increment(Counter storage param)을 사용할 수 있고, 이 때 함수의 인자(param)는 변수인 _tokenIds 자신이 된다.

0x0D: Rinkeby 로 배포한 뒤 OpenSea에서 확인하기

  • 이제 로컬 블록체인 네트워크가 아니라 실제 블록체인 네트워크컨트랙트를 배포하고 이를 이용해보도록 한다.
  • 대신, 테스트넷의 하나인 Rinkeby를 활용한다.

0x0E: 트랜잭션

  • 블록체인 네트워크상에서 블록체인에 변화를 주는 모든 행위는 트랜잭션이다.
  • 블록체인 네트워크상에 우리의 컨트랙트배포한다는 것은 대략 아래와 같다.
    • 블록체인 네트워크에 참여하는 모든 채굴자들에게 우리의 컨트랙트를 보낸다.
    • 채굴자들이 새로운 블록을 생성할 때, 우리의 컨트랙트를 포함한 블록을 생성한다.
    • 새로운 블록이 모든 채굴자들에게 브로드캐스트되면, 누구나 우리의 컨트랙트를 확인할 수 있다.
  • 이 때, 채굴자들에게 우리의 컨트랙트 정보를 전달하는 효과적인 방법으로 Alchemy( 링크 )를 이용하는 것이 있다.
  • 링크를 통해 들어가서, 간단한 가입절차를 거친 뒤 테스트넷을 위한 API Key를 확보하면 된다.

0x0F: 테스트넷

  • 이더리움 메인넷이 아니라 테스트넷을 이용하는 이유는 다음과 같다.
    • 메인넷과 유사한 환경에서 프로젝트를 테스트 하는 것이 가능하다.
    • 가짜 돈을 사용하기 때문에 테스트에 들어가는 비용이 없다.
  • 테스트넷에서 해 볼 것들은 아래와 같다.
    • 우리의 트랜잭션브로드캐스트하기
    • 실제 채굴자들에 의해 선택되기를 기다리기
    • 블록으로 만들어지기를 기다리기
    • 다른 채굴자들에게 우리의 트랜잭션이 포함된 새로운 블록브로드캐스트되기를 기다리기

0x10: 가짜 돈 얻기

  • 개인적으로 ( 링크 ) 가 가장 효율적으로 테스트를 위한 가짜 돈을 확보할 수 있는 방법 같다.
  • 한 번에 0.1 ETH를 받을 수 있지만, 이 정도면 테스트에는 충분하다.

0x11: 스크립트(deploy.js) 작성

  • 이제 배포를 위한 스크립트deploy.jsrun.js와 따로 구분해서 작성하도록 한다.
const main = async () => {
  const nftContractFactory = await hre.ethers.getContractFactory('MyEpicNFT');
  const nftContract = await nftContractFactory.deploy();
  await nftContract.deployed();
  console.log("Contract deployed to:", nftContract.address);

  // Call the function.
  let txn = await nftContract.makeAnEpicNFT()
  // Wait for it to be mined.
  await txn.wait()
  console.log("Minted NFT #1")

  txn = await nftContract.makeAnEpicNFT()
  // Wait for it to be mined.
  await txn.wait()
  console.log("Minted NFT #2")
};

const runMain = async () => {
  try {
    await main();
    process.exit(0);
  } catch (error) {
    console.log(error);
    process.exit(1);
  }
};

runMain();

0x12: Rinkeby에 배포하기

  • 배포하기 전, hardhat.config.js를 아래와 같이 수정한다.
require('@nomiclabs/hardhat-waffle');

module.exports = {
  solidity: '0.8.0',
  networks: {
    rinkeby: {
      url: 'YOUR_ALCHEMY_API_URL',
      accounts: ['YOUR_PRIVATE_RINKEBY_ACCOUNT_KEY'],
    },
  },
};
  • Alchemy에서 가져온 API주소와 지갑개인 키를 입력한다.
  • 이후 아래 명령어를 입력하여 컨트랙트Rinkeby 네트워크상에 배포되는 것을 확인한다.
npx hardhat run ./scripts/deploy.js --network rinkeby
  • 사실, 출력은 없지만 코드내부에서 Mint를 했었으므로, opensea에 가보면 내 지갑NFT가 있는 것을 확인할 수 있다.

[0x01] On-Chain 에서 NFT Generate 하기


0x13: "On-Chain"의 의미와 중요성

  • 다음 상황을 가정해보자.
    • [1] 만약 이미지데이터를 저장하고 있는 링크의 사이트가 유효하지 않다면?
    • [2] 만약 metadata의 링크가 더 이상 유효하지 않다면?
  • 위 두 상황에 직면하게 되었을 때, NFT는 더 이상 가치가 없을 것이다
  • 이러한 상황에 대한 대책으로, 영원히 지속될 것이라 믿는 블록체인 상에 이 데이터들을 저장하는 방법이 있다.
  • 이를 On-Chain (온체인)이라 부르고, 이를 통해 가치가 지속되리라는 신뢰를 얻을 수 있어 매우 중요하다.

0x14: SVG

  • 보통 이러한 NFT이미지 데이터를 저장하는 방법으로 SVG를 이용한다.
  • SVG이미지코드를 통해 렌더링 할 수 있는 방법으로 활용할 수 있다.
  • ( 참고 )
  • ( SVG 그림 그려주는 사이트 )
  • SVG로 작성한 코드Base64를 이용해서 Encoding하여 활용할 수 있다.
  • SVG 코드encoding한 뒤, 아래 형식에 맞춰 URL로 사용할 수 있다.
data:image/svg+xml;base64,INSERT_YOUR_BASE64_ENCODED_SVG_HERE
  • 이제 코드이미지를 저장할 수 있으므로, 블록체인상에 쉽게 기록할 수 있음을 알 수 있다.

0x15: JSON metadata

  • JSON MetadataSVG와 마찬가지로 Base64를 이용해 On-Chain데이터를 기록할 수 있다.
  • Base64로 인코딩된 SVG를 이용해 JSON Metadata를 다시 작성하면 아래와 같다.
{
    "name": "EpicLordHamburger",
    "description": "An NFT from the highly acclaimed square collection",
    "image": "data:image/svg+xml;base64,INSERT_YOUR_BASE64_ENCODED_SVG_HERE"
}
  • 이제 이 JSON Metadata도 전부 Base64로 인코딩한다.
  • 인코딩 된 JSON Metadata는 아래의 형식에 맞게 완성한 다음, 우리가 앞서 컨트랙트에서 _setTokenURI()함수의 두 번째 인자로 전달한 JSON Metadata URI값으로 사용할 수 있다.
data:application/json;base64,INSERT_YOUR_BASE64_ENCODED_JSON_HERE

0x16: 컨트랙트 수정 및 재배포

  • 앞선 내용들을 토대로 컨트랙트를 수정한 뒤 배포하면 새로운 컨트랙트의 주소를 얻는다.

  • OpenSea에서 새로운 NFTMint된 것을 확인할 수 있다.

  • [ 링크 ]


0x17: 랜덤으로 NFT 생성하기

  • 이제 랜덤한 NFT를 생성해보도록 한다.
  • 이번 튜토리얼에서는 아래와 같이 서로 다른 단어들을 이어 붙이는 방법으로 NFT를 생성한다.

<랜덤에 관하여>

  • 블록체인 에서는 모든 정보가 공개되어 있고, 이는 곧 유사 난수를 만들더라도 모든 값들이 예측 가능함을 의미한다.
  • 다시 말해, 랜덤을 구현하는 것이 거의 불가능하다.
  • 따라서, 아래와 같이 코딩하여 유사 난수를 사용하도록 한다.
uint256(keccak256(string(abi.encodePacked("FIRST_WORD", Strings.toString(tokenId)))));
  • 우선, 우리의 NFT에서 사용할 단어와 tokenIdabi.encodePacked() 로 연결한다.

Solidity에서 Concatenation을 하는 방법으로 abi.encodePacked()를 사용하는 것이 가능하다.

  • 패킹한 값을 string으로 바꾼다.
  • 마지막으로, random()을 통해 난수를 생성한다.
  • 랜덤을 구현하는 방법으로 ( 링크 ) 에서 소개된 방법을 이용할 수도 있다.

0x18: 컨트랙트 수정하기

  • 기존의 컨트랙트에서 NFT를 생성하는 함수는 아래와 같다.
  • tokenURI값이 하드코딩된 부분을 포함해 컨트랙트 전체를 수정한다.

<추가할 부분들>

  • SVG데이터에 대한 베이스 값으로 아래와 같이 변수를 하나 선언한다.
  • 배경색과 글자의 크기, 폰트, 위치 등을 지정한다.
string baseSvg = "<svg xmlns='http://www.w3.org/2000/svg' preserveAspectRatio='xMinYMin meet' viewBox='0 0 350 350'><style>.base { fill: white; font-family: serif; font-size: 24px; }</style><rect width='100%' height='100%' fill='black' /><text x='50%' y='50%' class='base' dominant-baseline='middle' text-anchor='middle'>";
  • 이어 붙일 세 단어 묶음을 선언한다.
  • 나는 주어 + 동사 + 목적어 형태로 NFT를 생성해보려 한다.
string[] firstWords = ["Davaron", "Alice", "Bryan", "Spencer", "Stephani", "Danny"]; 
string[] secondWords = ["eats", "takes", "hits", "makes", "throws", "has"]; 
string[] thirdWords = ["chicken", "baseball", "computer", "cookies", "battery", "time"];
  • 랜덤값을 반환하는 함수 random()을 선언한다.
  • 랜덤값을 만드는 방법으로, keccak256()함수를 활용한다.
  function random(string memory input) internal pure returns (uint256) {
      return uint256(keccak256(abi.encodePacked(input)));
  }
  • 각각의 단어 묶음에서 하나씩 랜덤으로 단어를 뽑아내는 함수를 만든다.
function pickRandomWords(uint256 tokenId) public view returns (string memory) {
  uint256 rand;
  string sentence;
 
  rand = random(string(abi.encodePacked("FIRST_WORD", Strings.toString(tokenId))));
  rand = rand % firstWords.length;
  sentence += firstWords[rand];
 
  rand = random(string(abi.encodePacked("SECOND_WORD", Strings.toString(tokenId))));
  rand = rand % secondWords.length;
  sentence += secondWords[rand];
 
  rand = random(string(abi.encodePacked("THIRD_WORD", Strings.toString(tokenId))));
  rand = rand % thirdWords.length;
  sentence += thirdWords[rand];
 
  return sentence; 
}
  • NFT를 생성하는 함수, makeAnEpicNFT()를 위에 추가한 부분들에 맞춰 아래와 같이 수정한다.
function makeAnEpicNFT() public {
  uint256 newItemId = _tokenIds.current();
 
  string memory sentence = pickRandomWords(newItemId);
 
  string memory finalSvg = string(abi.encodePacked(baseSvg, sentence, "</text></svg>"));
 
  console.log("\n=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=");
  console.log("SVG data : " + finalSvg);
  console.log("=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=\n");
 
  string memory json = Base64.encode(
    bytes(
      string(
        abi.encodePacked(
          '{"name": "', sentence, '",
            "description": "A highly acclaimed collection of squares.",
            "image": "data:image/svg+xml;base64,', Base64.encode(bytes(finalSvg))
       , '"}'
        )
      )
    )
  );
 
  string memory finalTokenUri = string(abi.encodePacked("data:application/json;base64,", json));
 
  _safeMint(msg.sender, newItemId);
 
  _setTokenURI(newItemId, finlaTokenUri);
 
  _tokenIds.increment();
  console.log("An NFT w/ ID %s has been minted to %s", newItemId, msg.sender);

<전체 코드>

  • 전체 코드는 ( [깃허브] ) 를 참조하면 된다.
  • 컨트랙트의 실행 결과는 아래와 같다.

0x19: Rinkeby에 배포

  • 배포하기 전 deploy.js 코드를 아래와 같이 조금 수정한다.
  • 반복문을 돌려서 총 16개의 NFTMint해보도록 한다.
  • 굉장히 느린 속도로 NFT가 하나 하나 Minting되는 것을 확인할 수 있다.
  • 컨트랙트 주소로 향해서 NFT를 확인해본다.

<디버깅>

  • 이미지가 안보이는 문제가 발생했다..
  • 천천히 디버깅해보니, SVG를 입력하는 과정에서 오타가 있었다...
  • 하지만.. 위 문제를 해결했는데도 이미지 랜더링이 되지 않았다.
  • 가능성은 아래와 같다.
    • Base64 코드의 결함
    • 컨트랙트 내 코드의 결함
  • 우선 Base64의 경우 아래와 같이 정상적으로 작동함을 알 수 있다.
  • 따라서, 문제는 컨트랙트 코드에 있음을 생각할 수 있다.
  • 튜토리얼에서 제공하는 예제 코드와 내가 작성한 코드를 비교해본다.
  • 그 결과, SVG에 대한 구문에서 한 글자가 다른 것을 찾아냈다.
  • http로 적어야 할 것을 https로 적어서 생긴 문제였던 것이다.
  • 해당 코드를 수정한 후에는 정상적으로 tokenURI를 이용할 수 있었다.
  • ( NFT Preview Site )를 통해 정상 작동 함을 확인했다.
  • 디버깅을 끝내고 다시 시도해보았다.
  • 새로운 컨트랙트 주소가 출력되고, NFTMint되는 것을 확인할 수 있다.
  • OpenSea를 통해 MintingNFT를 확인할 수 있다.

[0x02] 웹 클라이언트 구축


0x1A: Replit

  • 이전 튜토리얼과 마찬가지로 이번에도 Replit을 이용한다.

  • 구축할 웹사이트에서 Metamask를 연결하고 앞서 만든 NFT Minting 컨트랙트를 이용해보도록 한다.

  • 튜토리얼에서 제공하는 템플릿을 포크하여 아래와 같이 간단히 사이트를 만들 수 있다.


0x1B: window.ethereum()

  • 지갑을 사이트에 연결한다는 것은 로그인하는 것과 같다고 생각하면 된다.
  • 이 때, window.ethereum()을 이용해서 쉽게 구현할 수 있다.
  • App.jsx에서 window.ethereum() 을 이용해 지갑 연결 여부를 체크하는 코드는 아래와 같다.
import React, { useEffect } from "react";

/* ... */
/* ... */

const App = () => {
  const checkIfWalletIsConnected = () => {
      /*
      * First make sure we have access to window.ethereum
      */
      const { ethereum } = window;

      if (!ethereum) {
        console.log("Make sure you have metamask!");
        return;
      } else {
        console.log("We have the ethereum object", ethereum);
      }
  }

/* ... */
/* ... */

useEffect(() => {
  checkIfWalletIsConnected();
}, [])

/* ... */
/* ... */

};

0x1C: 유저 계정 접근 권한 확인

  • window.ethereum()으로 지갑 연결 여부를 확인한 다음은 접근 권한을 확인해야 한다.
  • 이 단계를 거치고 나면, 스마트 컨트랙트를 호출할 수 있다.
  • 이에 관한 App.jsx의 코드 내용은 아래와 같다.
import React, { useEffect, useState } from "react";

/* ... */
/* ... */

const App = () => {

  /*
  * Just a state variable we use to store our user's public wallet. Don't forget to import useState.
  */
  const [currentAccount, setCurrentAccount] = useState("");
 
  /*
  * Gotta make sure this is async.
  */
  const checkIfWalletIsConnected = async () => {
    const { ethereum } = window;

    if (!ethereum) {
        console.log("Make sure you have metamask!");
        return;
    } else {
        console.log("We have the ethereum object", ethereum);
    }

    /*
    * Check if we're authorized to access the user's wallet
    */
    const accounts = await ethereum.request({ method: 'eth_accounts' });

    /*
    * User can have multiple authorized accounts, we grab the first one if its there!
    */
    if (accounts.length !== 0) {
      const account = accounts[0];
      console.log("Found an authorized account:", account);
      setCurrentAccount(account)
    } else {
      console.log("No authorized account found")
    }
  }

/* ... */
/* ... */

useEffect(() => {
  checkIfWalletIsConnected();
}, [])

/* ... */
/* ... */

};
  • 수정된 부분은 주석이 달린 부분으로 아래와 같다.
    • useState() 를 사용하여 계정 정보를 저장
    • async키워드로 연결 체크를 비동기로 처리
    • 계정 접근 권한 유무를 확인
    • 유저의 계정들 중 맨 첫 번째 계정을 사용

0x1D: 버튼과 기능 연결

  • 이제 버튼컴포넌트와 구현한 기능을 연결한다.
  • 아래의 코드를 App() 내부에 코딩하면 된다.
  /*
  * Implement your connectWallet method here
  */
  const connectWallet = async () => {
    try {
      const { ethereum } = window;

      if (!ethereum) {
        alert("Get MetaMask!");
        return;
      }

      /*
      * Fancy method to request access to account.
      */
      const accounts = await ethereum.request({ method: "eth_requestAccounts" });

      /*
      * Boom! This should print out public address once we authorize Metamask.
      */
      console.log("Connected", accounts[0]);
      setCurrentAccount(accounts[0]); 
    } catch (error) {
      console.log(error)
    }
  }
 
  // Render Methods (onClick={connectWallet})
    const renderNotConnectedContainer = () => (
      <button onClick={connectWallet} className="cta-button connect-wallet-button">
        Connect to Wallet
      </button>
    );
  • 그 밖에, 계정이 연결된 후에는 아래의 코드와 같이 버튼을 다르게 보여준다든지 하는 추가적인 기능을 구현할 수 있다.
{currentAccount === "" ? (
	renderNotConnectedContainer()
) : (
	<button onClick={null} className="cta-button connect-wallet-button">
		Mint NFT
	</button>
)}
  • 우선은 Mint NFT 버튼에 대해 onClicknull로 정해둔다.

0x1E: 웹 사이트를 통한 NFT Mint

  • 이제 웹 사이트를 통해서 NFTMint하는 기능을 구현해본다.
  • 아래 코드App.jsx에서 connectWallet()함수 안에 추가한다.
const askContractToMintNft = async () => {
  const CONTRACT_ADDRESS = "INSERT_YOUR_DEPLOYED_RINKEBY_CONTRACT_ADDRESS";

  try {
    const { ethereum } = window;

    if (ethereum) {
      const provider = new ethers.providers.Web3Provider(ethereum);
      const signer = provider.getSigner();
      const connectedContract = new ethers.Contract(CONTRACT_ADDRESS, myEpicNft.abi, signer);

      console.log("Going to pop wallet now to pay gas...")
      let nftTxn = await connectedContract.makeAnEpicNFT();

      console.log("Mining...please wait.")
      await nftTxn.wait();
    
      console.log(`Mined, see transaction: https://rinkeby.etherscan.io/tx/${nftTxn.hash}`);

    } else {
      console.log("Ethereum object doesn't exist!");
    }
  } catch (error) {
    console.log(error)
  }
}
  • 코드를 차근 차근 살펴보자.
const provider = new ethers.providers.Web3Provider(ethereum);
const signer = provider.getSigner();
  • ehters프론트엔드컨트랙트가 상호작용할 수 있도록 돕는 라이브러리이다.
  • 코드 상단에 import { ethers } from "ethers";를 추가하여 사용할 수 있다.
  • Provider이더리움 노드와 상호작용 하기 위해 사용한다.
  • Alchemy를 통해 컨트랙트배포했던 것과 마찬가지로, 여기서는 Metamask를 이용해 이더리움 노드들과 통신한다.
  • Signer에 관한 내용은 ( 링크 )를 참조하면 된다.
const connectedContract = new ethers.Contract(CONTRACT_ADDRESS, myEpicNft.abi, signer);
  • 컨트랙트와 실제로 연결(connection)하도록 하는 코드이다.
  • 컨트랙트 주소, ABI 파일, 그리고 signer가 인자로 필요하다.
  • 블록체인 상의 컨트랙트와 상호작용 하기 위해서는 항상 이 세 가지가 필요하다.
console.log("Going to pop wallet now to pay gas...")
let nftTxn = await connectedContract.makeAnEpicNFT();

console.log("Mining...please wait.")
await nftTxn.wait();

console.log(`Mined, see transaction: https://rinkeby.etherscan.io/tx/${nftTxn.hash}`);
  • 나머지 코드는 앞서 deploy.js에서도 작성했던 코드이다.
  • 마지막으로, 앞서 Null로 설정했던 onClick 부분을 수정한다.
return (
  {currentAccount === "" 
    ? renderNotConnectedContainer()
    : (
      /** Add askContractToMintNft Action for the onClick event **/
      <button onClick={askContractToMintNft} className="cta-button connect-wallet-button">
        Mint NFT
      </button>
    )
  }
);

0x1F: ABI

  • ABI컨트랙트컴파일 할 때 생성되는 파일이다.
  • hardhat컴파일 작업 후, artifacts/디렉토리에 ABI파일을 저장해둔다.
  • ABI웹 앱컨트랙트와 상호작용 할 때 필요한 파일이다. ( 링크 )
  • Replit을 이용해서 웹 앱을 만들 때는, artifacts/contracts/MyEpicNFT.sol/MyEpicNFT.json 에 존재하는 ABI파일의 내용을 복사한 뒤 Replit으로 돌아와 src/utils/MyEpicNFT.json에 저장하여 활용할 수 있다.
  • 그런 다음, 아래 한 줄을 App.jsx에 추가하면 된다.
import myEpicNft from './utils/MyEpicNFT.json';
  • 이것으로 웹 앱에서 NFTMinting하는 것도 모두 구현해 보았다.
  • 전체 App.jsx 파일의 내용은 ( 깃허브 ) 를 참조하면 된다.

0x20: 웹 마무리

  • OpenSea에서 확인할 수 있는 NFT는 아래 형식의 URL을 따라 확인할 수 있다.
https://testnets.opensea.io/assets/INSERT_CONTRACT_ADDRESS_HERE/INSERT_TOKEN_ID_HERE
https://testnets.opensea.io/assets/0x40553d47327d0ffcfc155480f4cad52a086c505d/16/sell
  • 또는 rarible 사이트를 이용해서도 확인할 수 있다.
https://rinkeby.rarible.com/token/INSERT_CONTRACT_ADDRESS_HERE:INSERT_TOKEN_ID_HERE
https://rinkeby.rarible.com/token/0x40553d47327d0ffcfc155480f4cad52a086c505d:16?tab=details
  • 현재 만들어 놓은 웹 사이트(웹 앱)tokenId를 알지 못하므로, 컨트랙트를 수정해서 이를 추가해주도록 한다.
  • 이 때, SolidityEvent를 사용할 것이다.
  • Eventwebhook같은 역할로 활용할 수 있다.
  • event 키워드로 이벤트를 선언하고, emit 키워드로 해당 이벤트를 호출할 수 있다.
event NewEvent(address sender, uint256 data);
/* ... */
emit NewEvent(msg.sender, data);
  • 트랜잭션블록으로 채굴될 때, NFT가 반드시 Mint되는 것은 아니기 때문에 Event를 통해 컨트랙트웹 앱에게 NFT가 정상적으로 Mint되었음을 알릴 수 있게 된다.
  • 컨트랙트를 수정하였으므로, 아래 세 단계를 거쳐 전체 파일을 수정한다.

    • Rinkeby로 다시 배포
    • App.jsx컨트랙트 주소를 업데이트함.
    • ABI 파일을 업데이트 함.
  • 마지막으로, 프론트엔드 코드에 아래 코드 등을 추가하면 끝난다. ( 전체 코드 )

connectedContract.on("NewEpicNFTMinted", (from, tokenId) => {
	console.log(from, tokenId.toNumber())
	alert(`Hey there! We've minted your NFT. It may be blank right now. It can take a max of 10 min to show up on OpenSea. Here's the link: <https://testnets.opensea.io/assets/${CONTRACT_ADDRESS}/${tokenId.toNumber()}>`)
});

profile
He11o W0r1d

0개의 댓글