NFT
는 특정한 데이터
와 연결지을 수 있는 하나의 토큰
이다.토큰
이 유일무이
하다는 것이다.NFT
에 대해 좀 더 자세히 이해해보도록 한다.(그림 출처)
스케치 그림
100장을 그릴 것이다.유일무이
하다.갤러리
를 통해 스케치 그림
들을 판매할 수 있다.스케치 그림
을 피카소가 그렸다고 확신하는데 그 이유는 다음과 같다.스케치 그림
을 그렸다고 주장하기 때문이다.서명
이 그림에 있기 때문이다.스케치 그림
을 판매한 갤러리
에서 ""피카소 오리지널""
임을 증명하는 증명서
를 주었기 때문이다.증명서
가 장기적으로 유효할 것이라 믿어야 하지만, 이는 매우 간단하다. 그냥 믿으면 되기 때문이다.NFT 영역
에서 생각해보자.JPEG 파일
같은 디지털 그림
을 100장 그린 뒤 판매한다고 가정한다.스마트 컨트랙트
를 작성하여 자신의 NFT Collection
을 만들 것이다.유일무이한 구별자
를 부여할 것이다. (Sketch#1, Sketch#2, Sketch#3, 등등)NFT Collection
을 출시
할 준비가 되었을 때, 그는 자신의 이더리움 지갑
을 이용해서 NFT Collection
에 서명
한다.NFT
를 판매하는 것은 단순히 NFT
를 구매자의 지갑 주소로 transfer
하는 과정이다.피카소 오리지널
을 피카소 본인으로부터 받게 된다.피카소 오리지널
임을 증명
할 수 있다.
- 피카소가
서명
하고작성
한스마트 컨트랙트
로부터NFT
를 전송 받았기 때문에 이는피카소 오리지널
이다.- 피카소가 작품을 만들 때 부여했던
유일무이한 구별자
를 통해 이것이오리지널
임을 확인 할 수 있다.
유일무이
하고 가치 있게
만들 수 있다.NFT
는 예술 작품
, 게임
등이 연관된 프로젝트가 아주 많이 진행되고 있다.로컬 이더리움 네트워크
를 통해 실습을 진행한다.스마트 컨트랙트
가 컴파일
되고 호출
되는 것을 로컬
상에서 실습할 수 있다.
스마트 컨트랙트
를 작성한다.- 작성한
스마트 컨트랙트
를블록체인
상에배포(deploy)
한다.웹사이트
를 만들어, 누구나 쉽게 우리의NFT Collection
을Mint
할 수 있도록 한다.
이더리움
이 어떻게 동작하는지를 이해하는데 좋은 자료 ( 링크 )hardhat
이라는 툴
을 이용하여 실습을 진행한다.Node
와 npm
을 이용해 설치할 수 있다.mkdir epic-nfts cd epic-nfts npm init -y npm install --save-dev hardhat
hardhat
에 대한 보다 자세한 설명은 ( [0x01] ) 또는 ( Hardhat Tutorial ) 두 곳을 참조하면 좋을 것 같다.hardhat
의 설치를 마쳤다면, 샘플 프로젝트
를 만들 수 있다.npx hardhat
Create a basic sample project
를 선택한 후 전부 Enter
키로 넘기면 아래와 같이 설치되는 과정이 나온다.hardhat-waffle
과 hardhat-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
Hardhat
으로스마트 컨트랙트
를컴파일
하여바이트코드
를 생성한다.Hardhat
은 일회성의로컬 블록체인
을 돌린다. 일회성이란,스마트 컨트랙트
의 실행이 끝나면 종료됨을 의미한다.Hardhat
이스마트 컨트랙트
를로컬 블록체인 네트워크
로배포(deploy)
한다.
스마트 컨트랙트
를 작성한다.컨트랙트
는 아래와 같다.// SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.4; import "hardhat/console.sol"; contract MyEpicNFT { constructor() { console.log("This is my NFT contract. Whoa!"); } }
컨트랙트
를 작성한 뒤, 다음 세 작업을 행해야 한다.
Hardhat
으로스마트 컨트랙트
를컴파일
하여바이트코드
를 생성한다.Hardhat
은 일회성의로컬 블록체인
을 돌린다. 일회성이란,스마트 컨트랙트
의 실행이 끝나면 종료됨을 의미한다.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();
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
를 이용해서 출력한다.
스크립트
를 실행한다.npx hardhat ./scripts/run.js
코드
내에서 자주 보이는 hre
에 대한 이해를 위해 ( 이 곳 ) 을 참조해도 좋다.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)
이라는 개념을 사용하고 있다. ( 참고 )NFT
는 ERC721
을 스탠다드로 한다. ( 참고 )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
라는컨트랙트
를 상속 받고 있다.- 이제
MyEpicNFT
는ERC721URIStorage
의 함수 등을 쓸 수 있다.
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()
에서는newItemId
를id
로 하여msg.sender
에게NFT
를Mint
한다.
msg.sender
를 사용하면 조금 더안전한 코딩
을 할 수 있다._setTokenURI()
에서는newItemId
를id
로 하는NFT
에 두 번쨰 인자를데이터
로 설정한다._tokenIds.increment()
를 통해카운트
값을 증가시킨다.
tokenURI
와 로컬에서 실행해보기tokenURI
란 NFT
의 실제 데이터가 있는 곳을 말한다.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
관련 서비스를 이용하는 데 문제가 발생할 수 있다.ERC721
의 metatdata
스탠다드는 이 세 가지 이며, 이를 따름으로써 보다 효과적으로 NFT
를 활용할 수 있다.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(); } }
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();
NFT
를 Mint
하여, 아래 명령어를 입력한 뒤 결과를 확인하면 아래와 같다.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
자신이 된다.
로컬 블록체인 네트워크
가 아니라 실제 블록체인 네트워크
에 컨트랙트
를 배포하고 이를 이용해보도록 한다.테스트넷
의 하나인 Rinkeby
를 활용한다.블록체인 네트워크
상에서 블록체인
에 변화를 주는 모든 행위는 트랜잭션
이다.블록체인 네트워크
상에 우리의 컨트랙트
를 배포
한다는 것은 대략 아래와 같다.블록체인 네트워크
에 참여하는 모든 채굴자
들에게 우리의 컨트랙트
를 보낸다.채굴자
들이 새로운 블록
을 생성할 때, 우리의 컨트랙트
를 포함한 블록
을 생성한다.새로운 블록
이 모든 채굴자
들에게 브로드캐스트
되면, 누구나 우리의 컨트랙트
를 확인할 수 있다.채굴자
들에게 우리의 컨트랙트
정보를 전달하는 효과적인 방법으로 Alchemy
( 링크 )를 이용하는 것이 있다.테스트넷
을 위한 API Key
를 확보하면 된다.이더리움 메인넷
이 아니라 테스트넷
을 이용하는 이유는 다음과 같다.메인넷
과 유사한 환경에서 프로젝트를 테스트
하는 것이 가능하다.가짜 돈
을 사용하기 때문에 테스트
에 들어가는 비용
이 없다.테스트넷
에서 해 볼 것들은 아래와 같다.트랜잭션
을 브로드캐스트
하기채굴자
들에 의해 선택되기를 기다리기블록
으로 만들어지기를 기다리기채굴자
들에게 우리의 트랜잭션
이 포함된 새로운 블록
이 브로드캐스트
되기를 기다리기테스트
를 위한 가짜 돈
을 확보할 수 있는 방법 같다.0.1 ETH
를 받을 수 있지만, 이 정도면 테스트
에는 충분하다.배포
를 위한 스크립트
인 deploy.js
를 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() 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();
배포
하기 전, 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
가 있는 것을 확인할 수 있다.NFT
는 더 이상 가치가 없을 것이다블록체인
상에 이 데이터
들을 저장하는 방법이 있다.On-Chain (온체인)
이라 부르고, 이를 통해 가치가 지속되리라는 신뢰
를 얻을 수 있어 매우 중요하다.NFT
의 이미지 데이터
를 저장하는 방법으로 SVG
를 이용한다.SVG
는 이미지
를 코드
를 통해 렌더링 할 수 있는 방법으로 활용할 수 있다.SVG
로 작성한 코드
를 Base64
를 이용해서 Encoding
하여 활용할 수 있다.SVG 코드
를 encoding
한 뒤, 아래 형식에 맞춰 URL
로 사용할 수 있다.data:image/svg+xml;base64,INSERT_YOUR_BASE64_ENCODED_SVG_HERE
코드
로 이미지
를 저장할 수 있으므로, 블록체인
상에 쉽게 기록할 수 있음을 알 수 있다.JSON Metadata
도 SVG
와 마찬가지로 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
앞선 내용들을 토대로 컨트랙트
를 수정한 뒤 배포하면 새로운 컨트랙트
의 주소를 얻는다.
OpenSea
에서 새로운 NFT
가 Mint
된 것을 확인할 수 있다.
[ 링크 ]
NFT
를 생성해보도록 한다.NFT
를 생성한다.블록체인
에서는 모든 정보가 공개되어 있고, 이는 곧 유사 난수
를 만들더라도 모든 값들이 예측 가능함을 의미한다.랜덤
을 구현하는 것이 거의 불가능하다.유사 난수
를 사용하도록 한다.uint256(keccak256(string(abi.encodePacked("FIRST_WORD", Strings.toString(tokenId)))));
NFT
에서 사용할 단어와 tokenId
를 abi.encodePacked()
로 연결한다.
Solidity
에서Concatenation
을 하는 방법으로abi.encodePacked()
를 사용하는 것이 가능하다.
string
으로 바꾼다.random()
을 통해 난수를 생성한다.
랜덤
을 구현하는 방법으로 ( 링크 ) 에서 소개된 방법을 이용할 수도 있다.
컨트랙트
에서 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);
컨트랙트
의 실행 결과는 아래와 같다.deploy.js
코드를 아래와 같이 조금 수정한다.NFT
를 Mint
해보도록 한다.NFT
가 하나 하나 Minting
되는 것을 확인할 수 있다.컨트랙트
주소로 향해서 NFT
를 확인해본다.
이미지
가 안보이는 문제가 발생했다..
- 천천히 디버깅해보니,
SVG
를 입력하는 과정에서 오타가 있었다...
- 하지만.. 위 문제를 해결했는데도 이미지 랜더링이 되지 않았다.
- 가능성은 아래와 같다.
Base64
코드의 결함컨트랙트
내 코드의 결함- 우선
Base64
의 경우 아래와 같이 정상적으로 작동함을 알 수 있다.
- 따라서, 문제는
컨트랙트
코드에 있음을 생각할 수 있다.- 튜토리얼에서 제공하는 예제 코드와 내가 작성한 코드를 비교해본다.
- 그 결과,
SVG
에 대한 구문에서한 글자
가 다른 것을 찾아냈다.http
로 적어야 할 것을https
로 적어서 생긴 문제였던 것이다.- 해당 코드를 수정한 후에는 정상적으로
tokenURI
를 이용할 수 있었다.- ( NFT Preview Site )를 통해 정상 작동 함을 확인했다.
디버깅
을 끝내고 다시 시도해보았다.컨트랙트
주소가 출력되고, NFT
가 Mint
되는 것을 확인할 수 있다.OpenSea
를 통해 Minting
한 NFT
를 확인할 수 있다.이전 튜토리얼과 마찬가지로 이번에도 Replit
을 이용한다.
구축할 웹사이트
에서 Metamask
를 연결하고 앞서 만든 NFT Minting
컨트랙트
를 이용해보도록 한다.
튜토리얼에서 제공하는 템플릿을 포크
하여 아래와 같이 간단히 사이트를 만들 수 있다.
지갑
을 사이트에 연결한다는 것은 로그인
하는 것과 같다고 생각하면 된다.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(); }, []) /* ... */ /* ... */ };
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
키워드로 연결 체크
를 비동기로 처리계정 접근 권한
유무를 확인계정
들 중 맨 첫 번째
계정
을 사용버튼
컴포넌트와 구현한 기능을 연결한다.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
버튼에 대해 onClick
을 null
로 정해둔다.NFT Mint
웹 사이트
를 통해서 NFT
를 Mint
하는 기능을 구현해본다.코드
를 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> ) } );
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';
웹 앱
에서 NFT
를 Minting
하는 것도 모두 구현해 보았다.App.jsx
파일의 내용은 ( 깃허브 ) 를 참조하면 된다.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
를 알지 못하므로, 컨트랙트
를 수정해서 이를 추가해주도록 한다.Solidity
의 Event
를 사용할 것이다.Event
는 webhook
같은 역할로 활용할 수 있다.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()}>`) });