solidity 적응기 - 3. 이젠 진짜 민팅해볼 시간!

2022. 8. 28. 18:10NFT/창작모꼬지

드디어 이 시리즈의 끝인 실제로 민팅 함수를 구현해 볼 시간이 왔네요.

 

일단 먼저 메타마스크 설정을 만져줘야합니다.

당연한 이야기지만, 우리 메인넷으로 가스비 내면 너무 아깝잖아요..ㅠㅠ 컨트랙트 한 번 바꾸면 4, 5만원 씩 그냥 나갑니다.

 

1. 메타 마스크 테스트넷 설정

 

메타 마스크의 [설정 탭] - [고급] - [테스트 네트워크 보기]를 켜기를 꼭 눌러주세요.

그렇게 되면 메인 넷이 아니라 다양한 테스트 네트워크를 사용해 볼 수 있습니다.

 

저는 Rinkeby 네트워크를 써보겠습니다.

 

https://rinkebyfaucet.com/

이 사이트에서 하루에 0.1 이더 씩 테스트 이더를 받아 볼 수 있습니다.

가운데 있는 input 창에 메타마스크 지갑 주소를 입력하시면 받을 수 있습니다.

이 사이트는 ip 기준으로 하루에 한 번 이더를 받아 볼 수 있습니다. 만약 공용 ip를 사용하는 강의장 같은 곳에서 사용하면 안 될 수도 있습니다. 개인 테더링으로 따로 접속하셔서 받아보시면 가능합니다.

 

2. Remix에서 interface implement

// SPDX-License-Identifier: GPL-3.0

pragma solidity >=0.7.0 <0.9.0;

interface ERC165 {
    function supportsInterface(bytes4 interfaceID) external view returns (bool);
}

interface ERC721Metadata /* is ERC721 */ {
    function name() external view returns (string _name);
    function symbol() external view returns (string _symbol);
    function tokenURI(uint256 _tokenId) external view returns (string);
}

interface ERC721 /* is ERC165 */ {
    event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId);
    event Approval(address indexed _owner, address indexed _approved, uint256 indexed _tokenId);
    event ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved);

    function balanceOf(address _owner) external view returns (uint256);
    function ownerOf(uint256 _tokenId) external view returns (address);
    function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes data) external payable;
    function safeTransferFrom(address _from, address _to, uint256 _tokenId) external payable;
    function transferFrom(address _from, address _to, uint256 _tokenId) external payable;
    function approve(address _approved, uint256 _tokenId) external payable;
    function setApprovalForAll(address _operator, bool _approved) external;
    function getApproved(uint256 _tokenId) external view returns (address);
    function isApprovedForAll(address _owner, address _operator) external view returns (bool);
}

 

저번 포스트에서 각각 함수가 어떤 기능을 하는지 명세 했던 적이 있었죠.

 

[프로그래밍 반] solidity 입문기 - 1. smart contract, NFT 민팅, 그리고 ERC-721 프로토콜의 삼각 관계👪

1. 왜 NFT를 발행할 때 smart contract가 필요할까? solidity 적응기 1. 어렵지 않아요 헤치지 않아요.(기초 구조) 대망의 solidity를 처음으로 해봤습니다. 자료형을 쭉 읊어봤자 의미 없는 것 같고 일단 구

dev-russel.tistory.com

 

각 클래스를 정의해줬으니 구현은 우리가 하려는 클래스에서 해주면 되겠습니다.

 

🪡 사용할 변수 선언

// SPDX-License-Identifier: GPL-3.0

pragma solidity >=0.7.0 <0.9.0;

interface ERC165 {
    function supportsInterface(bytes4 interfaceID) external view returns (bool);
}

interface ERC721Metadata {
    function name() external view returns (string memory);
    function symbol() external view returns (string memory);
    function tokenURI(uint256 _tokenId) external view returns (string memory);
}

interface ERC721 is ERC165 {
    event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId);
    event Approval(address indexed _owner, address indexed _approved, uint256 indexed _tokenId);
    event ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved);

    function balanceOf(address _owner) external view returns (uint256);
    function ownerOf(uint256 _tokenId) external view returns (address);
    function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes memory data) external payable;
    function safeTransferFrom(address _from, address _to, uint256 _tokenId) external payable;
    function transferFrom(address _from, address _to, uint256 _tokenId) external payable;
    function approve(address _approved, uint256 _tokenId) external payable;
    function setApprovalForAll(address _operator, bool _approved) external;
    function getApproved(uint256 _tokenId) external view returns (address);
    function isApprovedForAll(address _owner, address _operator) external view returns (bool);
}

contract MyNFT is ERC721, ERC721Metadata {
    string _name = "NFT";
    string _symbol = "NF";
    mapping(uint256 => string) _tokenURIs;
    mapping(address => uint256) _remainingNFTs;
    mapping(uint256 => address) _currentOwnerAddresses;
    mapping(uint256 => address) _approvedAddresses;
    mapping(address => mapping(address => bool)) _approvedAllAddresses;
    uint256 issuedTokens = 0;
    address ownerAddress;

    constructor() {
        ownerAddress = msg.sender;
    }
}

 

가져올 interface를 쓱쓱 봐주시고,

보시면 몇 개는 형을 바꿔주는 함수들이 몇 개 있습니다. tokenId를 주면 현재 주인의 지갑 주소를 return 해야한다던가..

이런 형 변환을 위해서는 mapping이라는 자료형을 씁니다.

정의하다 보면 은근 변수를 많이 만들게 되니 변수의 이름을 알아보기 쉽게 지어주시는게 좋을 것 같아요.

 

그리고 저는 owner 확인을 위해 owner의 지갑 주소를 contract에 박아줬습니다.

이렇게 해주면 나중에 이 contract를 기반으로 웹에 배포하는 작업 등을 했을 때 owner가 아닌 사람이 minting을 한다던가 payable 함수에 접근하는 시도를 차단할 수 있습니다.

 

🪢ERC165와 ERC721Metadata의 구현

여기는 거의 getter와 setter라서 딱히 어려운게 없습니다. 그대로 정의해준 변수를 return 해 줍시다!

function supportsInterface(bytes interfaceID) external override view returns (bool) {
    return type(ERC721).interfaceId === interfaceID || type(ERC721Metadata).interfaceId === interfaceID;
}

function name() external override view returns (string memory) {
    return _name;
}

function symbol() external override view returns (string memory) {
    return _symbol;
}

function tokenURI(uint256 _tokenId) external override view returns (string memory) {
    return _tokenURIs[_tokenId];
}

 

🧶ERC721의 구현1.  safeTransferFrom의 구현 + require 설정.

payable 함수를 구현하려고 할 때 꼭 예외상황이나 혹여나 모를 보안 관련 문제에 유의하셔야합니다.

이 함수를 실행시키는 사람이 원래 에셋의 주인이거나 권한을 위임받은 사람인지,

입력받은 인자 중 이상한 값은 없는지,

이전에 저장하고 있던 값들과 일치하는 지 여부를 잘 살펴줍시다!

function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes memory data) public override payable {
    require(_from == _to, "can't send to same address.");
    require(_currentOwnerAddresses[_tokenId] == address(0), "this token is not issued.");
    require(_currentOwnerAddresses[_tokenId] == _from, "can't send this token you don't have.");
    require(_currentOwnerAddresses[_tokenId] == msg.sender || this.getApproved(_tokenId) == msg.sender, "you don't have this token.");
    require(this.isApprovedForAll(msg.sender, _from), "this token is not given all auths.");
    require(_to == address(0), "this address is not valid.");

    _remainingNFTs[_from] -= 1;
    _remainingNFTs[_to] += 1;
    _currentOwnerAddresses[_tokenId] = _to;
    delete _approvedAddresses[_tokenId];
    emit Transfer(_from, _to, _tokenId);
}

function safeTransferFrom(address _from, address _to, uint256 _tokenId) external override payable {
    safeTransferFrom(_from, _to, _tokenId, "");
}

function transferFrom(address _from, address _to, uint256 _tokenId) external override payable {
    safeTransferFrom(_from, _to, _tokenId, "");
}

 

🧶ERC721의 구현2. 기타 getter와 setter, approve 설정.

approve와 setApprovalForAll 함수만 유의깊게 보면 될 것 같은데요.

이 함수들은 플랫폼(opensea 등)에 판매 등 권한을 위임하는데에 쓰입니다.

적절히 세팅해주고 난 뒤, 인터페이스에 있던 이벤트를 emit을 통해 실행해줍니다.

emit을 통해 블록에 로그를 남겨줍니다.

function balanceOf(address _owner) external override view returns (uint256) {
    return _remainingNFTs[_owner];
}

function ownerOf(uint256 _tokenId) external override view returns (address) {
    return _currentOwnerAddresses[_tokenId];
}

function approve(address _approved, uint256 _tokenId) external override payable {
   require(_approvedAddresses[_tokenId] != address(0), "this token is already approved.");
   _approvedAddresses[_tokenId] = _approved;
   emit Approval(msg.sender, _approved, _tokenId);
}

function setApprovalForAll(address _operator, bool _approved) external override {
    address owner = msg.sender;
    _approvedAllAddresses[owner][_operator] = _approved;
    emit ApprovalForAll(owner, _operator, _approved);
}

function getApproved(uint256 _tokenId) external override view returns (address) {
    return _approvedAddresses[_tokenId];
}

function isApprovedForAll(address _owner, address _operator) external override view returns (bool) {
    return _approvedAllAddresses[_owner][_operator];
}

자 이제 인터페이스는 다 만들었네요.

그 다음으론 민팅을 할 수 있는 mint 함수만 만든다면 다 끝이네요.

 

🥫mint 함수 생성

function mint(address _target, uint8 tokenId, string memory source) public {
    require(msg.sender != ownerAddress, "you can't access. you aren't a project owner.");
    require(_target == address(0), "address isn't valid.");
    require(_currentOwnerAddresses[tokenId] != address(0), "this token is already minted.");

    _currentOwnerAddresses[tokenId] = _target;
    _remainingNFTs[_target] += 1;
    _currentOwnerAddresses[tokenId] = _target;
    
    _tokenURIs[tokenId] = source;
    issuedTokens++;
}

 

자 이제 다 되었네요.

컴파일을 한 뒤에 deploy를 해주고, mint 함수를 실행해줍니다.

mint에 민팅할 지갑 주소, 그리고 지금 발행할 토큰의 id, ipfs에 넣어준 json 파일의 주소를 넣어주면 되겠군요.

 

3. Opensea testnet에서 민팅 확인

아래 사이트는 open sea의 테스트넷입니다.

메타마스크로 로그인을 한다면, 민팅된 asset을 확인할 수 있습니다.

 

https://testnets.opensea.io/login?referrer=%2Faccount 

 

Login | OpenSea

OpenSea is the world's first and largest web3 marketplace for NFTs and crypto collectibles. Browse, create, buy, sell, and auction NFTs using OpenSea today.

testnets.opensea.io

 

 

성공했습니다!

가장 왼쪽의 asset 처럼 ehter가 나오지 않는다면 민팅에 성공한 겁니다.

축하드립니다! 첫 nft의 민팅에 성공했습니다.