MetaData
는 NFT
의 핵심으로 JSON 파일로 되어있으며 아래와 같은 내용을 가지고 있습니다.
NFT
의 이름
NFT
에 대한 설명
- 호스팅된 이미지의 링크
- 특성들
- 이 외에 어떤 값이든 추가할 수 있지만
OpenSea
와 같은 NFT Market
플랫폼에서 요구하는 변수명으로 입력하는 내용만 화면에 보여줄 수 있습니다.
NFT 구매 및 판매 Contract 작성
open-zeppelin
라이브러리를 설치하여 작업하였고 remix IDE
를 사용하여 배포 및 테스트를 해볼 것입니다.
NFT를 발행할 Contract
pragma solidity ^0.8.15;
import "./node_modules/openzeppelin-solidity/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
import "./node_modules/openzeppelin-solidity/contracts/access/Ownable.sol";
import "./node_modules/openzeppelin-solidity/contracts/utils/Strings.sol";
contract SwanToken is ERC721Enumerable, Ownable {
uint constant public MAX_TOKEN_COUNT = 1000;
uint public mint_price = 1 ether;
string public metadataURI;
constructor(string memory _name, string memory _symbol, string memory _metadataURI) ERC721(_name,_symbol){
metadataURI = _metadataURI;
}
struct TokenData {
uint Rank;
uint Type;
}
mapping(uint => TokenData) public TokenDatas;
uint[4][4] public tokenCount;
}
- 설치한
open-zeppelin
라이브러리에서 필요한 파일들을 import
하여 상속받거나 import
한 파일의 함수를 사용하였습니다.
Token
의 최대 발행량의 제한을 두기 위해 constant
속성을 사용하여 값을 변경 못하도록 상수로 만들어주고 최대 발행량을 1000으로 제한하였습니다.
- 생성자 함수를 통해 인자값으로 발행할 토큰의 이름, 심볼명,
json
파일인 metadata
의 URI
를 인자값으로 받고 토큰 이름과 심볼명을 가지고 새로운 토큰을 만들어주었고, 인자값으로 받은 _metadataURI
을 미리 선언한 metadataURI
에 할당하여 주었습니다. 이 metadata URI
는 후에 많은 metadata
들의 URI
에 baseURI
역할을 할 것입니다.
- 발행한
NFT
마다 다른 json
파일을 가리키기위해 TokenData
구조체를 만들어줍니다. 후에 Rank
와 Type
으로 서로 다른 metadata URI
를 가르키도록 처리할 것입니다.
tokenId
에 따른 TokenData
를 알고 값을 입력해주기 위해 TokenDatas mapping
을 만들어줍니다.
- 각각의
NFT
들이 Rank
와 Type
마다 몇개씩 발행되었는지 사용자에게 알려주기 위해 2차 배열로 tokenCount
상태변수를 만들었습니다
function mintToken() public payable{
require(msg.value == mint_price);
require(MAX_TOKEN_COUNT > totalSupply());
uint tokenId = ERC721Enumerable.totalSupply() + 1;
TokenDatas[tokenId] = getRandomNum(msg.sender, tokenId);
tokenCount[TokenDatas[tokenId].Rank - 1][TokenDatas[tokenId].Type - 1] += 1;
payable(Ownable.owner()).transfer(msg.value);
_mint(msg.sender, tokenId);
}
function mintToken
- 실제로
ETH
를 받고 NFT
를 발행해줄 mintToken
함수를 만들어줍니다. ETH
에 대한 거래를 포함하는 함수이니 payable
함수로 만들어줍니다.
require()
를 사용하여 받은 ETH
가 NFT
를 발행할 ETH
와 같은지 확인하고 NFT
를 1000개까지만 발행하도록 검증하는 코드를 추가하였습니다.
NFT
의 고유한 값인 tokenId
는 총 발행된 NFT
의 갯수의 + 1을하여 최초 발행시에는 1, 그후로 1씩 Auto Increment
처리하였습니다.
- 발행한
NFT
마다 metadata
의 구분을 짓기 위한 Rank
와 Type
을 getRandomNum
함수를 사용하여 랜덤한 값으로 할당받도록 처리하였습니다. 뒤에서 getRandomNum
함수의 code
를 살펴보도록 하겠습니다.
- 발행한
NFT
가 Rank
1, Type
1 이란 metadataURI
를 가진 NFT
로 만들어졌다면 2차 배열인 tokenCount
의 해당 Rank
와 Type
에 위치한 index
의 Count
를 + 1하여 Rank
1, Type
1이란 NFT
가 1개 발행되었다고 알 수 있도록 하였습니다.
NFT
를 발행할 때 받은 ETH
를 CA
계정이 아닌 해당 Contract
를 배포한 EOA
계정에게 바로 ETH
를 보내주었습니다.
- 마지막으로
_mint
함수를 사용하여 함수를 실행한 계정에게 발행한 NFT
의 소유권을 할당하였습니다.
function tokenURI(uint _tokenId) public override view returns(string memory) {
string memory Rank = Strings.toString(TokenDatas[_tokenId].Rank);
string memory Type = Strings.toString(TokenDatas[_tokenId].Type);
return string(abi.encodePacked(metadataURI,"/",Rank,"/",Type,".json"));
}
function tokenURI
open-zeppelin
에서 import
해온 Strings
파일의 toString
함수를 사용하여 uint Type
으로 반환되는 Rank
와 Type
의 값을 byte code
로 변환 후 다시 string type
으로 변환하여 memory
에 저장해주었습니다.
tokenId
에 Random
값인 Rank
와 Type
으로 NFT
의 metadata
인 json
파일의 URI
값을 만들어 반환해주었습니다.
function getRandomNum(address _owner, uint _tokenId) private pure returns(TokenData memory){
uint randomNum = uint(keccak256(abi.encodePacked(_owner, _tokenId)))%100;
TokenData memory data;
if (randomNum < 5) {
if (randomNum == 1) {
data.Rank = 4;
data.Type = 1;
} else if (randomNum == 2) {
data.Rank = 4;
data.Type = 2;
} else if (randomNum == 3) {
data.Rank = 4;
data.Type = 3;
} else {
data.Rank = 4;
data.Type = 4;
}
} else if (randomNum < 13) {
if (randomNum < 7) {
data.Rank = 3;
data.Type = 1;
} else if (randomNum < 9) {
data.Rank = 3;
data.Type = 2;
} else if (randomNum < 11) {
data.Rank = 3;
data.Type = 3;
} else {
data.Rank = 3;
data.Type = 4;
}
} else if (randomNum < 37) {
if (randomNum < 19) {
data.Rank = 2;
data.Type = 1;
} else if (randomNum < 25) {
data.Rank = 2;
data.Type = 2;
} else if (randomNum < 31) {
data.Rank = 2;
data.Type = 3;
} else {
data.Rank = 2;
data.Type = 4;
}
} else {
if (randomNum < 52) {
data.Rank = 1;
data.Type = 1;
} else if (randomNum < 68) {
data.Rank = 1;
data.Type = 2;
} else if (randomNum < 84) {
data.Rank = 1;
data.Type = 3;
} else {
data.Rank = 1;
data.Type = 4;
}
}
return data;
}
function getRandomNum
abi
의 encodePacked
메서드를 사용하여 Type
과 상관없이 인자값의 값들을 이어붙이고 그 값을 keccak256
메서드를 사용하여 해쉬화하고 그값을 다시 uint type
으로 변환해주고 % 100으로 그 값의 마지막 두자리만 가져옵니다.
TokenData Type
의 data
를 memory
로 선언하고 if
문으로 일정 확률로 Rank
와 Type
의 값을 할당해주고 해당 값을 return
하였습니다.
- 이렇게
Random
한 요소로 생성돠는 NFT
의 Rank
와 Type
으로 구매자의 NFT metadata
를 마치 뽑기처럼 랜덤으로 처리해주었습니다.
function setMetaDataURI(string memory _uri) public onlyOwner {
metadataURI = _uri;
}
function getTokenRank(uint _tokenId) public view returns(uint) {
return TokenDatas[_tokenId].Rank;
}
function getTokenType(uint _tokenId) public view returns(uint) {
return TokenDatas[_tokenId].Type;
}
function getTokenCount() public view returns(uint[4][4] memory) {
return tokenCount;
}
function setMetaDataURI
- 혹시 후에
metadata URI
를 수정해야 할 수도있으니 metadata URI
을 수정하는 함수를 만들어줍니다. onlyOwner
속성을 사용하여 해당 함수는 Contract
를 배포한 사람만 수정할 수 있도록 처리하였습니다.
function getTokenRank
tokenId
를 입력하였을 때 해당 NFT
의 Rank
를 return
해줍니다.
function getTokenType
tokenId
를 입력하였을 때 해당 NFT
의 Type
를 return
해줍니다.
function getTokenCount
NFT
들이 Rank
와 Type
마다 몇개씩 발행되었는지를 알 수 있는 tokenCount
를 return
해줍니다. 굳이 새로운 getter
함수를 만드는 이유는 기존 배열의 getter
함수는 하나의 값만 return
해주었지만 지금은 배열의 모든 값을 확인해야 하는 상황입니다. 이같은 상황에서 memory
속성을 return
값에 추가하면 함수 내부적으로 배열의 모든값들을 memory
에 새로 추가하여 그 값을 return
해주는 것 같습니다.
NFT를 구매 및 판매할 Contract
pragma solidity ^0.8.15;
import "./SwanToken.sol";
contract SaleToken {
SwanToken public Token;
constructor(address _tokenAddress) {
Token = SwanToken(_tokenAddress);
}
struct TokenInfo {
uint tokenId;
uint Rank;
uint Type;
uint price;
}
mapping(uint => uint) public tokenPrices;
uint[] public SaleTokenList;
}
NFT
를 발행할 SwanToken Contract
의 CA
값을 생성자 함수 인자값에 넣어 Token
이라는 상태 변수에 할당하여 SwanToken Contract
의 메서드들을 사용할 수 있도록 하였습니다.
NFT
의 메타 데이터에 들어가는 내용들을 화면에 보여주기 편하도록 TokenInfo
라는 구조체를 선언하였습니다.
- 마지막으로
NFT
의 판매가를 설정할 tokenPrices
매핑과 NFT의 판매중 여부를 판단하는 SaleTokenList
배열을 선언하였습니다.
function SalesToken(uint _tokenId, uint _price) public {
address tokenOwner = Token.ownerOf(_tokenId);
require(tokenOwner == msg.sender);
require(_price > 0);
require(Token.isApprovedForAll(msg.sender, address(this)));
tokenPrices[_tokenId] = _price;
SaleTokenList.push(_tokenId);
}
function SalesToken
NFT
보유자가 NFT
를 판매 등록할 함수를 만들어줍니다. NFT
의 소유자를 가져오고 소유자와 함수를 실행한 사람이 같은지 확인하고 Token
에 있는 isApprovedForAll
메서드를 사용하여 SaleToken
의 CA
값이 NFT
판매자의 대리인이 맞는지 확인합니다.
- 위의 검증을 모두 통과하였으면
tokenPrices
에 해당 NFT
의 tokenId
값과 판매가를 할당하고 판매중인 NFT
목록인 SaleTokenList
에 판매할 tokenId
값을 넣어줍니다.
function BuyToken(uint _tokenId) public payable {
address tokenOwner = Token.ownerOf(_tokenId);
require(tokenOwner != msg.sender);
require(tokenPrices[_tokenId] > 0);
require(tokenPrices[_tokenId] <= msg.value );
payable(tokenOwner).transfer(msg.value);
Token.transferFrom(tokenOwner, msg.sender, _tokenId);
tokenPrices[_tokenId] = 0;
popSaleToken(_tokenId);
}
function BuyToken
NFT
판매 등록함수를 만들었으니 이제 NFT
를 구매할 함수를 만들어줍니다. 구매하려는 NFT
의 tokenId
값을 가지고 NFT
의 소유자를 가져온 후 구매자와 NFT
판매자가 혹시 같은지 확인합니다. 또한 구매자가 구매하려는 NFT
가 현재 판매중인지 확인하고 마지막으로 구매자가 NFT
판매가에 알맞게 ETH
를 보냈는지 확인합니다.
- 위의 검증을 모두 통과할 경우 NFT는 환불 기능이 없으니 바로
CA
계정에서 받은 ETH
를 SaleToken Contract
를 배포한 EOA
계정에게 보내줍니다.
- 이제 판매중이던
NFT
를 판매 대리인이 구매자에게 보내줍니다. 판매가 완료된 NFT
니 판매중인 NFT
매핑인 tokenPrices
매핑에서 해당 tokenId
에 0을 할당하여 판매중이지 않은 NFT
로 처리합니다.
- 마지막으로 판매중인
NFT
배열에서 판매 완료된 NFT
를 제거하기 위해 popSaleToken
함수를 실행합니다. 해당 함수에 대한 해석은 뒤에서 설명하겠습니다.
function cancleSaleToken(uint _tokenId) public {
address tokenOwner = Token.ownerOf(_tokenId);
require(tokenOwner == msg.sender);
require(tokenPrices[_tokenId] > 0);
tokenPrices[_tokenId] = 0;
popSaleToken(_tokenId);
}
function cancleSaleToken
NFT
판매등록을 하고 취소하고 싶을수도 있으니 NFT
판매 취소를 처리할 함수를 만들어줍니다. 먼저 NFT
의 소유자를 가져오고 소유자와 함수를 실행한 계정과 같은지 확인하고 해당 NFT
가 판매중이던 상품이었는지 확인합니다.
NFT
판매 목록 매핑인 tokenPrices
에서 해당 tokenId
에 0을 할당하여 판매중이지 않은 NFT
로 처리합니다
- 마지막으로 판매중인
NFT
목록 배열에서 해당 NFT
를 제거하기위해 popSaleToken
함수를 실행합니다. 해당 함수에 대한 해석은 뒤에서 설명하겠습니다.
function popSaleToken(uint _tokenId) private returns(bool) {
for(uint i = 0; i < SaleTokenList.length; i++) {
if(SaleTokenList[i] == _tokenId) {
SaleTokenList[i] = SaleTokenList[SaleTokenList.length-1];
SaleTokenList.pop();
return true;
}
}
return false;
}
function popSaleToken
- 판매가 완료되거나 판매를 취소할때 판매중인
NFT
배열에서 해당 NFT
를 제거하는 함수를 만들어줍니다.
- 반복문을 사용하여 판매중인
NFT
목록인 SaleToken
안에 모든 NFT
들을 확인하여 판매 목록에서 제거할 NFT
의 tokenId
와 일치하는 NFT
를 찾으면 해당 index
에 위치한 값을 배열 마지막 index
의 값으로 변경합니다. 이렇게하면 배열의 마지막 index
의 위치했던 tokenId
값은 2개가 되고 제거해야할 tokenId
값은 사라지게 되었을것입니다. 이제 pop
메서드를 사용하여 배열의 마지막 index
의 tokenId
값을 제거하여 중복된 tokenId
값을 하나 제거합니다.
function getSaleTokenList() public view returns(TokenInfo[] memory){
require(SaleTokenList.length > 0);
TokenInfo[] memory list = new TokenInfo[](SaleTokenList.length);
for(uint i = 0; i < SaleTokenList.length; i++) {
uint tokenId = SaleTokenList[i];
uint Rank = Token.getTokenRank(tokenId);
uint Type = Token.getTokenType(tokenId);
uint price = tokenPrices[tokenId];
list[i] = TokenInfo(tokenId, Rank, Type, price);
}
return list;
}
function getSaleTokenList
- 판매중인 모든
NFT
들을 화면에 보여줄 함수를 만들어줍니다. TokenInfo type
에 맞는 list
라는 새로운 배열을 선언하는데 memory
속성을 사용하여 해당 함수 내에서만 사용하도록 하였고 판매중인 NFT
목록인 SaleTokenList
와 같은 길이로 생성하였습니다.
- 판매중인
NFT
목록인 SaleTokenList
을 모두 확인하여 tokenId
값을 가져온뒤 해당 Token
에 해당하는 Rank
와 Type
는 SwanToken Contract
에서 생성한 메서드들을 가지고 각각 구해오고 해당 NFT
의 판매가격을 가지고 있는 매핑을 통해 해당 tokenId
의 price
를 구해옵니다.
- 구해온 값들을
list
배열안에 새로 할당합니다. list
배열안에는 각 Token
별 tokenInfo
구조체가 들어갈 것입니다.
- 마지막으로 각
Token
에 정보들이 담긴 list
배열을 return
합니다.
function getOwnerTokens(address _tokenOwner) public view returns(TokenInfo[] memory){
uint balance = Token.balanceOf(_tokenOwner);
require(balance != 0);
TokenInfo[] memory list = new TokenInfo[](balance);
for(uint i = 0; i < balance; i++) {
uint tokenId = Token.tokenOfOwnerByIndex(_tokenOwner, i);
uint Rank = Token.getTokenRank(tokenId);
uint Type = Token.getTokenType(tokenId);
uint price = tokenPrices[tokenId];
list[i] = TokenInfo(tokenId, Rank, Type, price);
}
return list;
}
function getOwnerTokens
- 서비스 이용자가 본인이 소유하고 있는
NFT
를 확인하는 함수를 만들어줍니다. 인자값으로 지갑주소를 받고 해당 계정이 가지고 있는 NFT
가 있는지 확인합니다.
- 위의 검증을 마쳤다면 본인의
balance
, 즉 본인이 소유하고 있는 NFT
갯수와 같은 길이의 배열을 만들어줍니다. 마찬가지로 함수 내에서만 사용하는 상태변수로 memory
속성을 사용하였습니다.
- 반복문을 돌려 사용자가 소유하고 있는
NFT
를 모두 확인하여 위의 getSaleTokenList
함수와 같은 방식으로 Token
의 정보들을 가져오는데 tokenId
를 가져올때만 tokenOfOwnerByIndex
함수를 사용하여 소유자의 n
번째있는 NFT
의 tokenId
를 가져오도록 합시다.
- 마찬가지로 소유자의 모든
Token
정보가 list
배열에 구조체로 들어갈 것입니다. 마지막으로 각 Token
에 정보들이 담긴 list
배열을 return
합니다.
function getLatestToken(address _tokenOwner) public view returns(TokenInfo memory) {
uint balance = Token.balanceOf(_tokenOwner) - 1;
uint tokenId = Token.tokenOfOwnerByIndex(_tokenOwner,balance);
uint Rank = Token.getTokenRank(tokenId);
uint Type = Token.getTokenType(tokenId);
uint price = tokenPrices[tokenId];
return TokenInfo(tokenId,Rank,Type,price);
}
function getLatestToken
- 마지막으로 사용자가
NFT
를 Minting
할 때 어떤 Metadata
를 가진 NFT
인지 알 수 있도록 마지막 NFT
정보를 가져오는 함수를 만들어줍니다.
- 마찬가지로
memory
속성을 사용하고 balanceOf
함수를 사용하여 소유자의 보유한 NFT
갯수를 가져오는데 - 1을 하여 해당 값을 index
로하여 tokenId
를 가져옵니다.
- 이제 해당
NFT
의 Rank
, Type
를 구해오고 price
는 필요없지만 TokenInfo
의 type
에 price
가 있으니 넣어주어 해당 구조체를 return
합니다.