오픈제플린은 컨트랙트 개발을 쉽게 도와주는 라이브러리이다. ERC20, ERC721에 대한 형식이 다 있어서 사용법을 보고 따라쓰면 된다.
ERC721 토큰에 들어가보면 ERC721과 IERC721이 있는데, ERC721은 토큰의 명세가 구현되어있고, IERC721은 인터페이스 역할을 합니다.
아래와 같이 다중상속을 이용해 구현한 것을 확인할 수 있다.
배포는 Truffle을 사용하고 사용할 네트워크는 로컬환경에서 쉽게 테스팅 해볼 수 있는 ganache를 사용한다.
ganache-cli를 통한 로컬테스팅 커맨드
트러플로 배포하기 위해서는 아래의 명령어를 사용하면 된다.
truffle migrate --compile-all --reset --network ganache
배포가 끝나면 build폴더에 json형식으로 컨트랙트들이 저장된다.
배포한 컨트랙트의 노드로 들어가기 위해서는 아래의 명령어를 사용하면 된다.
truffle console --network ganache
배포된 컨트랙트의 노드에 들어가 컨트랙트 인스턴스를 받아올 수 있다.
instance 변수에 컨트랙트를 담아올 수 있다. 또한 컨트랙트에 ERC721FULL 컨트랙트를 상속받아와 생성자에 이름과 심볼을 넣었기 때문에 instance.name()으로 이름을 받아올 수 있다.
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);
}
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라는 분산 네트워크를 사용해 데이터를 저장한다.
메타데이터를 저장하고 받아온 해쉬값이다.
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가지로 나뉜다.
먼저 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;
}
토큰 판매를 걸어놨으면 토큰을 살 수 있게 로직을 만들어주어야 한다.
토큰 구매에 유효성 검사는 두가지가 있다.
위의 두가지 유효성 검사가 끝나면 판매자 계정으로 돈을 송금하고 구매자 계정으로 토큰을 전송하고 토큰 가격을 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;
}
}
컨트랙트가 판매자의 토큰을 대신 팔수 있도록 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)
안녕하세요 구글링을 하다가 글 올려주신거 보고 이렇게 댓글 남깁니다
현재 ntf 컬렉션을 제작하기위해 프로젝트 팀을 구성하여 진행중인데, 클레이튼 기반의 nft를 제작할 수 있는 개발자분을 찾고 있습니다!^^ 자세한 내용 함께 이야기 나누고 픈데 괜찮으시다면 010 6812 7600으로 연락 부탁드리겠습니다