지난 1주차에는 블록체인에 대한 간단한 개념과 아이디어톤이었다면,
2주차에는 앞으로 만들 앱을 간단히 살펴보고, NFT와 마켓앱에 관한 스마트 컨트랙트를 작성해본다.
<Intro ~ BApp 시연>
이번 강에서 무엇을 배울지, 각 주차에 무엇을 배울지 알아보고, 후에 만들 BApp을 시연하였다.
<Hello, Klaytn>
우선은 클레이튼 월렛 사이트에서 컨트랙트를 배포할 지갑을 만든다.
만든 지갑에서 클레이를 주고 받을수도 있고, 컨트랙트를 배포할 수도 있다.
Klaytn Wallet
wallet.klaytn.com
지갑을 만든 후 위 사진의 빨간 동그라미에서 메인넷을 바오밥 테스트 넷으로 바꿀 수 있다.
우리는 테스트넷에서 배포를 할 것이다.
네트워크가 나눠져있는 이유는
컨트랙트를 배포할때 돈이 들기도 하고, 배포한 후의 컨트랙트는 수정이 불가능하기 때문에
테스트 넷에서 여러 상황을 테스트해보고 배포를 할 수 있게끔 테스트 환경을 제공하는 것이다.
Create Account를 눌러 키를 만든다.
키를 만드는 과정에서 나오는 키들은 모두 안전한 곳에 보관해야만 하는데,
네트워크에 연결되었다는 것은 해킹 가능성이 있다는 말이니 네트워크와 떨어진 형태로 저장하는 것이 제일 좋다.
Address는 사실은 공개해도 상관없지만 내 트랜잭션 내용을 비밀로하고 싶다면 숨기는 것이 좋다.
클레이튼에는 이더리움과 달리 주소가 3가지인데, 사실 왜 3개인지는 모르겠다.
강의에서는 아래와같이 설명해주시는데 wallet key가 private키와 비슷하다고만 하고 넘어가셔서,
나중에 내가 따로 알아봐야 할 것 같다.
Address : 계좌번호(공개해도 됨)
Klaytn Wallet Key : 계좌
Private key : 비밀번호
테스트 넷에는 Faucet이라는 메뉴가 있는데 여기서는 테스트할 수 있는 클레이를 무료로 받을 수 있다.
물론 말 그대로 테스트용으로 주어지는 것이기 때문에 값어치는 없다.
받은 클레이는 클레이튼 스코프에서 확인할 수 있다.
클레이튼 스코프에서는 클레이튼 네트워크에서 일어나는 모든 거래내역을 볼 수 있다.
Klaytnscope
Klaytnscope allows you to find data by monitoring network health and statistics of Klaytn as well as profiling blocks and transactions on Klaytn.
scope.klaytn.com
만든 지갑을 가지고 클레이튼 ide에서 컨트랙트를 작성 배포할 수 있다.
Klaytn IDE
ide.klaytn.com
<스마트 컨트랙트 개념>
블록체인은 월드컴퓨터이다.
누구나 볼 수 있고 누구나 사용할 수 있다.
클레이튼 계좌 종류에는 두 가지가 있다.
개인키 기반 계좌 (주소, 잔액)
스마트 컨트랙트 계좌 (주소, 잔액, 코드)
블록체인은 각각의 계좌가 주고받은 트랜잭션의 집합이다.
(트랜잭션 : 블록체인에서의 활동들 ex. 돈(klay) 거래, 컨트랙트 배포, 실행...)
각각의 트랜잭션에는 수수료가 든다.(수수료 = gas * gasprice)
<NFT발행 - 솔리디티 기초1>
이 강의에서는 컴파일러를 version:0.5.6 버전으로 사용합니다.
pragma solidity >=0.4.24 <=0.5.6;
contract Practice {
//private와 public의 예시 + 값을 저장 가능 하다는 것을 보여줌
uint private totalSupply = 10;
string public name = "HEOM";
//address 와 mapping 함수
address public owner; // contract deployer
mapping (uint256 => string) public tokenURIs;
//최초 1회만 실행되는 함수
constructor () public {
owner = msg.sender;
}
//값을 더하는 함수
function getTotalSupply() public view returns (uint256) {
return totalSupply + 10000000;
}
// require 사용 예시, 최초발행자만 totalSupply값을 바꿀 수 있음
function setTotalSupply(uint256 newSupply) public {
require(owner == msg.sender, 'Not owner');
totalSupply = newSupply;
}
// mapping 함수 사용 예시, id 와 uri를 넣어주고 id를 넣어 uri를 확인 할 수 있다.
function setTokenUri(uint256 id, string memory uri) public {
tokenURIs[id] = uri;
}
}
private - 블록체인상에서 확인 불가(코드가 아닌 변수, 함수에 들어있는 값을 확인 불가능하다)
public - 블록체인상에서 확인 가능
view 함수 - 보기만 하는 함수
값을 바꿀 때는 function에 returns나 view를 표시 안 해줘도 됨.
블록체인상에서 값을 읽는 것에는 수수료가 들지 않지만 값을 바꿀 때에는 트랜잭션을 블록체인상에 올리기 때문에 수수료가 듦.
constructor - 컨트랙트가 배포되자마자 1회만 실행되는 함수
require - 조건을 만족하면 계속해서 실행하고, 만족하지 못하면 error처리 (if문과 비슷하다고 보면 될 것 같다)
js에서 다음과 같이 key와 value를 묶는 변수를 map형식의 변수라고 부른다.
솔리디티에도 mapping이라는 것이 있다.
<FT vs NFT>
FT - 대체 가능한(ex. 1000원)
NFT - 대체 불가능한(ex. 그림, 사진, 하나하나가 유니크한 것들)
NFT에 필요한 정보 -> 글자(글, 사진, 영상 모두 컴퓨터는 글자로 받아들인다), 소유자
필요한 기능 -> 1. 발행(일련번호, 글자, 소유자) 2. 전송(누가, 누구에게, 무엇을)
<NFT발행 - 솔리디티 기초2>
pragma solidity >=0.4.24 <=0.5.6;
contract Practice {
string public name = "HEOM";
string public symbol = "HM";
mapping (uint256 => address) public tokenOwner;
mapping (uint256 => string) public tokenURIs;
//소유한 토큰 리스트
mapping(address => uint256[]) private _ownedTokens;
//mint (tokenId, uri, owner)
//transferFrom(from, to, tokenId) -> owner가 바뀌는 것 (from -> to)
function mintWithTokenURI(address to, uint256 tokenId, string memory tokenURI) public returns (bool) {
// to 에게 tokenId(일련번호)를 발행하겠다.
// 적힐 글자는 tokenURI
tokenOwner[tokenId] = to;
tokenURIs[tokenId] = tokenURI;
// add token to the list
_ownedTokens[to].push(tokenId);
return true;
}
function safeTransferFrom(address from, address to, uint256 tokenId) public {
require(from == msg.sender, "from != msg.sender");
require(from == tokenOwner[tokenId], "you are not the owner of the token");
//
_removeTokenFromList(from, tokenId);
_ownedTokens[to].push(tokenId);
//
tokenOwner[tokenId] = to;
}
function _removeTokenFromList(address from, uint256 tokenId) private {
// [10, 15, 19, 20] -> 19번을 삭제하고 싶어요
// [10, 15, 20, 19]
// [10, 15, 20]
uint256 lastTokenIndex = _ownedTokens[from].length - 1;
for (uint256 i = 0; i < _ownedTokens[from].length; i++) {
if (tokenId == _ownedTokens[from][i]) {
//Swap last token with deleting token;
_ownedTokens[from][i] = _ownedTokens[from][lastTokenIndex];
_ownedTokens[from][lastTokenIndex] = tokenId;
break;
}
}
//
_ownedTokens[from].length--;
}
function ownedTokens(address owner) public view returns (uint256[] memory) {
return _ownedTokens[owner];
}
function setTokenUri(uint256 id, string memory uri) public {
tokenURIs[id] = uri;
}
}
전 차시에서는 솔리디티의 사용법을 대충 알아보았다면, 이번시간에는 NFT의 기본적인 뼈대를 대충 만들어보는 시간을 가졌다.
로직을 대충 설명하면,
mintWithTokenURI 로 token을 만들어 _ownedTokens라는 리스트에 넣어 어떤 토큰들을 가지고 있는지 한번에 확인 할 수 있게 한다.
safeTransferFrom 로 토큰을 전송할 수 있고 전송할 때에 _removeTokenFromList 함수를 이용하여,
기존 소유자가 가지고 있던 _ownedTokens 리스트에서 해당 토큰을 삭제한다.
_ownedTokens리스트의 내역은 ownedTokens함수에 address를 넣어 확인할 수 있다.
setTokenUri 함수는 저번시간에 쓰던 함수를 삭제하지 않은 것인데, 그닥 필요하지는 않아보인다.
다만 수업시간에서 그냥 넘어가서 나도 지우지 않았다.
<Market - 컨트랙트 연동하기>
contract NFTMarket {
function buyNFT(uint256 tokenId, address NFTAddress, address to) public returns (bool) {
NFTsimple(NFTAddress).safeTransferFrom(address(this), to, tokenId);
//사용하려는 컨트랙트명(컨트랙트의주소).함수명(인자들)
return true;
}
}
컨트랙트는 서로 호출하고 호출될 수 있다.
컨트랙트의 주소를 호출해서 위의 코드블럭에 주석을 달아놓은 방식대로 사용하면 된다.
address안에 있는 this는 NFTMarket 컨트랙트의 주소이다.(본인)
buyNFT는 NFTMarket이 가지고 있는 토큰을 to가 구매하는 함수이다.
<KIP -17, NFT>
지금까지는 공부를 위해서 간단하게 컨트랙트를 만들어 보았고, 이번 시간은 우리가 실제로 사용 할 KIP-17을 사용해 볼 것이다.
실제 NFT 스마트 컨트랙트를 만들기 위해서는 더 많은 코드들이 필요하다.
또한 직접 컨트랙트를 만들고 사용하는 것에는 위험이 있기 때문에 인증된 컨트랙트를 사용하는 것이 좋다.
여기서 KIP는 Klaytn Improvement Proposals의 줄임말로,
Klaytn 블록체인은 퍼블릭 블록체인이고,
퍼블릭 블록체인이라는 말은 누구든지 이 오픈소스에 기여해서 Klaytn 블록체인을 향상시킬 수 있다라는 뜻이다.
개발자 뿐 아니라 누구나 네트워크의 향상을 위한 의견을 제시할 수 있는데,
이러한 제안들을 모아 놓은 것이 KIP이다.
아래 사이트에서 kip 관련된 정보들을 얻을 수 있다.
Klaytn Improvement Proposals (KIPs) | Klaytn Improvement Proposals
Klaytn Improvement Proposals (KIPs) describe standards for the Klaytn platform, including core protocol specifications, client APIs, and contract standards.
kips.klaytn.com
KIP 17에 대한 내용도 살펴 볼 수 있다
(상단 All을 클릭하면 주요 KIP를 확인 할 수 있는데 거기서 KIP 17을 클릭하면 된다)
<Market Contract>
이번 시간에는 나중에 실제 사용할 마켓 컨트랙트를 만들건데,
NFT를 사고 파는 과정을 테스트해보기 위해서
전에 만들었던 NFTsimple도 살짝 수정해서 마켓 컨트랙트와 연동되게 해보고,
KIP17도 마켓 컨트랙트와 연동이 되도록 만들 것이다.
마켓에 필요한 기능은
1. 발행, 조회
2. 판매 : Market에게 전송
3. 구매 : Market에서 buy 실행
이렇게 3가지인데, 1번은 이미 구현했고 2번 3번만 하면 된다.
Market에게 NFT를 전송하면 그 NFT는 매대에 올라간 상태가 되고,
누군가 그 NFT를 특정 가격을 내고 구매하면, buy함수를 실행시켜
그 돈은 판매자에게 가고 Market에서 구매자에게 NFT가 이동하도록 하면 된다.
전체 코드
pragma solidity >=0.4.24 <=0.5.6;
contract NFTsimple {
string public name = "HEOM";
string public symbol = "HM";
mapping (uint256 => address) public tokenOwner;
mapping (uint256 => string) public tokenURIs;
//소유한 토큰 리스트
mapping(address => uint256[]) private _ownedTokens;
// _KIP17_RECEIVED bytes value
bytes4 private constant _KIP17_RECEIVED = 0x6745782b;
//mint (tokenId, uri, owner)
//transferFrom(from, to, tokenId) -> owner가 바뀌는 것 (from -> to)
function mintWithTokenURI(address to, uint256 tokenId, string memory tokenURI) public returns (bool) {
// to 에게 tokenId(일련번호)를 발행하겠다.
// 적힐 글자는 tokenURI
tokenOwner[tokenId] = to;
tokenURIs[tokenId] = tokenURI;
// add token to the list
_ownedTokens[to].push(tokenId);
return true;
}
function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory _data) public {
require(from == msg.sender, "from != msg.sender");
require(from == tokenOwner[tokenId], "you are not the owner of the token");
//
_removeTokenFromList(from, tokenId);
_ownedTokens[to].push(tokenId);
//
tokenOwner[tokenId] = to;
//만약에 받는 쪽이 실행할 코드가 없는 컨트랙트이면 코드를 실행할 것
require(
_checkOnKIP17Received(from, to, tokenId, _data), "KIP17 : transfer to non KIP!&Receiver implementer"
);
}
function _checkOnKIP17Received(address from, address to, uint256 tokenId, bytes memory _data) internal returns (bool) {
bool success;
bytes memory returndata;
if (!isContract(to)) {
return true;
}
(success, returndata) = to.call(
abi.encodeWithSelector(
_KIP17_RECEIVED,
msg.sender,
from,
tokenId,
_data
)
);
if (
returndata.length != 0 &&
abi.decode(returndata, (bytes4)) == _KIP17_RECEIVED
) {
return true;
}
return false;
}
function isContract(address account) internal view returns (bool) {
uint256 size;
assembly { size := extcodesize(account)}
return size > 0;
}
function _removeTokenFromList(address from, uint256 tokenId) private {
// [10, 15, 19, 20] -> 19번을 삭제하고 싶어요
// [10, 15, 20, 19]
// [10, 15, 20]
uint256 lastTokenIndex = _ownedTokens[from].length - 1;
for (uint256 i = 0; i < _ownedTokens[from].length; i++) {
if (tokenId == _ownedTokens[from][i]) {
//Swap last token with deleting token;
_ownedTokens[from][i] = _ownedTokens[from][lastTokenIndex];
_ownedTokens[from][lastTokenIndex] = tokenId;
break;
}
}
//
_ownedTokens[from].length--;
}
function ownedTokens(address owner) public view returns (uint256[] memory) {
return _ownedTokens[owner];
}
function setTokenUri(uint256 id, string memory uri) public {
tokenURIs[id] = uri;
}
}
contract NFTMarket {
//tokenID를 => 누가 보냈냐
mapping(uint256 => address) public seller;
function buyNFT(uint256 tokenId, address NFTAddress) public payable returns (bool) {
//구매한 사람한테 0.01 KLAY 전송
//seller(판매자)가 0.01 klay를 받아야함
//payable을 붙여준 주소에만 코드상으로 클레이를 전송 가능하다
address payable receiver = address(uint256(seller[tokenId]));
// send 0.01 klay ot receiver
// 10 ** 18 PEB = 1 KLAY
// 10 ** 16 PEB = 0.01 KLAY
receiver.transfer(1 ** 16);
//NFT 토큰 구매자에게 전송
NFTsimple(NFTAddress).safeTransferFrom(address(this), msg.sender, tokenId, '0x00');
return true;
}
// 마켓이 토큰을 받았을 때(판매대에 올라갔을 때), 판매자가 누군지 기록해야함
function onKIP17Received(address operator, address from, uint256 tokenId, bytes memory data) public returns (bytes4) {
seller[tokenId] = from;
//스마트 컨트랙트가 토큰을 받았을 때 실행하는 함수들이 있는데, 어떤 함수를 실행할 지 리턴값을 정해서 리턴해주는 것이다.
return bytes4(keccak256("onKIP17Received(address,address,uint256,bytes)"));
}
}
특정 주소에 코드상으로 클레이를 전송하고 싶다면, payable을 꼭 붙여줘야한다.
또한 전송을 실행할 함수에도 payable을 붙여줘야 한다.
좀 어려울만한 함수를 가져오면 이건데,
일단 주석으로 최대한 설명을 붙여보았다.
function _checkOnKIP17Received(address from, address to, uint256 tokenId, bytes memory _data) internal returns (bool) {
bool success;
bytes memory returndata;
if (!isContract(to)) {
//스마트 컨트랙트가 아니면 리턴
return true;
}
//스마트컨트랙트면 실행하는 것들
//to.call(실행할 것들) -> to 주소에 가서 괄호 안의 것들을 실행하세요.
//실행 후에 성공결과를 success에 리턴값을 returndata에 받아와라
(success, returndata) = to.call(
abi.encodeWithSelector(
_KIP17_RECEIVED, //onKIP17Received 함수를 실행해라
msg.sender, // 여기서부터 _data까지는 onKIP17Received 함수에 들어갈 인자
from,
tokenId,
_data
)
);
// 리턴 데이터가 있고, _KIP17_RECEIVED와 같으면 true 리턴
if (
returndata.length != 0 &&
abi.decode(returndata, (bytes4)) == _KIP17_RECEIVED
) {
return true;
}
// 잘못 되면 false 리턴
return false;
}
_KIP17_RECEIVED는 컨트랙트 맨 위에
bytes4 private constant _KIP17_RECEIVED = 0x6745782b; 로 정의해줬는데
여기서 0x6745782b는 onKIP17Received을 16진수로 바꿔놓은 것인데
솔리디티에서 컴퓨터가 인식하기로는 onKIP17Received로 인식해서 onKIP17Received함수를 찾아간다고 한다.
이 부분에서 궁금한점이 두개가 있는데
첫번째가
_checkOnKIP17Received 함수에서 _KIP17_RECEIVED 의 값인 0x6745782b가 onKIP17Received함수를 뜻한다고 하셨는데, _checkOnKIP17Received 함수는 NFTsimple 컨트랙트 안에 있고, onKIP17Received 함수는 Market 컨트랙트 안에 있는데 어떻게 호출이 된다는건지...
그리구 두번째가
이 함수에서 bytes memory _data를 쓰지 않는데 왜 넣으셨을까... 그것은 의문이다.
쓰려고 넣었는데 막상 만들어보니 사용을 안한게 아닌가 싶기도 하고...
'강좌 & 책 > 멋사NFT' 카테고리의 다른 글
[4주차] NFT 블록체인 마켓 앱 만들기 with 그라운드X (3) | 2021.06.30 |
---|---|
[3주차] NFT 블록체인 마켓 앱 만들기 with 그라운드X (0) | 2021.06.30 |
[1주차] NFT 블록체인 마켓 앱 만들기 with 그라운드X (2) | 2021.05.14 |