React와 web3의 만남 - Web3-react로 NFT 민팅하기 (2) 🚀

Jeenie·2022년 9월 6일
0

세상을 바꿀 web 3.0

목록 보기
6/7
post-thumbnail

이전 포스트에서는 지갑 연결하기를 해봤다.

이 포스트에서는 대망의 NFT 민팅을 해볼 것이다!

민팅으로 어떻게 트랜잭션을 발생시키고,
그 트랜잭션은 실제 컨트랙트 상에서 어떻게 처리되는지 직접 눈으로 보면서 흐름을 이해해보자.

1. 민팅하기

1-1. 민팅(Minting)

  • 스마트 컨트랙트 민팅 로직
    이미 배포되어있는 스마트 컨트랙트 상에 토큰 아이디는 순차적으로 증가하고,
    민트 버튼을 누른 순간 해당 번째의 토큰 아이디를 받아온다는 로직이 짜여져있다는 전제를 두고 진행한다.
  • 프로젝트 구현 방향
    토큰 아이디는 미리 알 수 없고, 내가 민팅을 해서 소유권을 받는 순간 해당 토큰 아이디가 표시된다.

1-2. 민팅을 하려면?

민팅을 하기 위해서는 반드시 아래의 두가지가 필요하다.

  1. 스마트 컨트랙트의 주소
  2. 스마트 컨트랙트의 ABI

스마트 컨트랙트 주소와 ABI를 알면 web3.js를 통해서
스마트 컨트랙트 인스턴스를 생성하고, 그 인스턴스 내부의 메소드인 특정 함수들을 실행할 수 있다!

왜 스마트 컨트랙트 인스턴스를 생성하고, 특정 함수를 어떻게 실행하지?

: 우리가 만든 이 contractInstance는, ethers.Contract 객체가 가지고 있는 모든 메소드들을 상속받았기 때문에 !
📌 객체-인스턴스에 대한 자세한 내용은 [JavaScript] - 객체와 인스턴스에서

const contractInstance = new ethers.Contract(contractAddress, abi, library);
// 주소, ABI,그리고 library를 보내 컨트랙트 인스턴스를 생성하고
const data = await contractInstance.populateTransaction.mint(1);
// 해당 인스턴스가 가진 메소드 populateTransaction.mint(1);를 통해 민팅한다.

signer.populateTransaction( transactionRequest ) ⇒ Promise< TransactionRequest >

This is generally not required to be overridden, but may be needed to provide custom behaviour in sub-classes.
This should return a copy of transactionRequest, follow the same procedure as checkTransaction and fill in any properties required for sending a transaction. The result should have all promises resolved; if needed the resolveProperties utility function can be used for this.
populateTransaction ether.js 공식문서 발췌

signer.sendTransaction( transactionRequest ) ⇒ Promise< TransactionResponse >

This method populates the transactionRequest with missing fields, using populateTransaction and returns a Promise which resolves to the transaction.
Sub-classes must implement this, however they may throw if sending a transaction is not supported, such as the VoidSigner or if the Wallet is offline and not connected to a Provider.
sendTransaction ether.js 공식문서 발췌

1-3. 트랜잭션을 보내려면?

contract 인스턴스가 가지고 있는 populateTransaction 메소드와 sendTransaction 메소드를 사용한다.

1-3-1. populateTransaction

전송할 트랜잭션 object를 만들기 위해 populateTransaction 메소드를 호출한다.

const contractInstance = new ethers.Contract(address, abi);
const data = await contractInstance.populateTransaction.mint(1);
console.log("data", data);
/** data 출력결과
{ data: "0xa0712d680000000000000000000000000000000000000000000000000000000000000001",
  to: "contract address"}
*/

여기서 찍힌 저 이상하게 생긴 data의 값은 트랜잭션의 인풋 데이터!
(이더스캔의 트랜잭션 기록에서 확인 가능)

1-3-2. sendTransaction

네트워크에 트랜잭션을 전송하는 메소드.

그런데 트랜잭션을 전송하려면?
데이터 뿐만 아니라, 디지털 서명도 반드시 들어가야한다.

📌 트랜잭션 구조에 대한 자세한 설명은 블록체인(Block Chain) - 트랜잭션(Transaction)에서

sendTransaction로 트랜잭션을 전송하고 싶은데,
트랜잭션을 일으키기 위해서는 반드시 서명을 해야한다.

✨ 트랜잭션에 서명(Sign)하는 방법 🔏

    1. 잠금해제된 계정으로 서명하기 (Using unlocked accounts)
      : truffle-hdwallet-provider 같은 전문 Web3 공급자( =지갑 provider. 우리는 메타마스크의 provider를 사용 중 )를 사용하여 잠금 해제된 계정을 생성할 수도 있다.
    1. 개인키로 직접 서명하기 (Using a private key)
      : 트랜잭션을 시작하는 주소의 개인 키를 사용하여 트랜잭션에 서명할 수 있다. 하지만 개인 키를 다루는 건 매우 까다롭고 주의가 필요하다.

📌 이게 바로 세상을 바꿀 Web3 - web3.js란? 에서 설명한,
메타마스크가 제공하는 web3로 Web3 Provider를 초기화하는 이유 !!!!!
( How do I sign transactions with Web3? 참고 )

useWeb3Reactlibrary에는 web3 Provider가 들어있다.
그리고 그 Provider는 우리가 메타마스크의 provider로 초기화한 상태.

따라서 library 안의 getSigner()로, 잠금해제된 계정을 얻을 수 있다 !

const signer = library.getSigner();
// 잠금해제된 계정
const signedTransaction = await signer.sendTransaction(params);
// 잠금해제된 계정으로, 아까의 트랜잭션 구조 객체를 담아서 트랜잭션을 전송한다.
let reciept = await signedTransaction.wait();
console.log("reciept",reciept);

1-4. 따라하기

1-4-1. Contract Instance 생성

지금 web3.js를 사용하고 있으니 web3가 제공하는 컨트랙트 인스턴스 생성방식으로 하면 좋겠지만,
web3안에 Contract라는 메소드를 찾을 수 없어서 우선 1-2처럼 ethers가 제공하는 Contract 메소드를 사용해야겠다.

ethers 설치

yarn add ethers

ethers와 web3를 모두 설치할 필요가 없을텐데, 왜 둘다 필요한걸까?

주소와 ABI 넣기

나는 이미 배포되어있는 컨트랙트가 있기 때문에, 해당 컨트랙트의 주소와 ABI를 가져왔다.
ABI는 이더스캔에서 확인할 수 있다.

const handleMint = (address: string) => {
  const abi = [
    {
      inputs: [{ internalType: "uint256", name: "count", type: "uint256" }],
      name: "mint",
      outputs: [],
      stateMutability: "nonpayable",
      type: "function",
    },
  ];
  const contractInstance = new ethers.Contract(address, abi);
    
};

이렇게 컨트랙트의 인스턴스가 생성됐다.

1-4-2. 트랜잭션 보내기

import { useWeb3React } from "@web3-react/core";
import { Injected } from "App";

const Page = () => {
  const theme = useTheme();
  const { activate, deactivate, account, chainId } = useWeb3React();
  const [isMinting, setIsMinting] = useState(false);
  
  const handleConnect = () => {
    if ((window as any).ethereum === undefined) {
      window.open(
        `https://metamask.app.link/dapp/${window.location.host}`,
        "_blank"
      );
      return;
    }
    if (active && account) {
      deactivate();
    }
    activate(Injected);
  }
  const handleMint = async (address: string) => {
    const abi = [
      {
        inputs: [{ internalType: "uint256", name: "count", type: "uint256" }],
        name: "mint",
        outputs: [],
        stateMutability: "nonpayable",
        type: "function",
      },
    ];
    const contractInstance = new ethers.Contract(address, abi);
    const data = await contractInstance.populateTransaction.mint(1);
    setIsMinting(true);
    const signer = library.getSigner(); // 잠금해제된 계정을 받아왔다.
    const signedTransaction = await signer.sendTransaction(data);
    // 잠금해제된 계정으로, 아까의 트랜잭션 구조 객체를 담아서 트랜잭션을 전송한다.
    let reciept = await signedTransaction.wait();
    console.log("reciept", reciept);
    setIsMinting(false);
  };

  
  return (
    <Container>
      <StyledSampleDiv theme={theme}>
        <Typography variant="h4" color={theme.colors.gray[1000]}>
          Account: {account}
        </Typography>
        <Typography variant="h4" color={theme.colors.gray[1000]}>
          chainId: {chainId}
        </Typography>
        <Button onClick={handleConnect}>
          {active ? "연결 해제" : "지갑 연결하기"}
        </Button>
        <Button
          onClick={() => handleMint(constants.CONTRACT_ADDRESS)}
          color="primary"
          disabled={isMinting === true}
          isLoading={isMinting}
        >
          Minting
        </Button>
      </StyledSampleDiv>
    </Container>
  );
};

export default Page;

이렇게 민팅 로직을 완료했다.

이제 어떻게 민팅이 이루어지는지 눈으로 보자!

2. 민팅 테스트하기

2-1. 테스트 이더 받기

작업환경은 testnet인 goerli

테스트 이더를 받아오기 위해 GOERLI FAUCET에 접속한 뒤, 간단하게 구글로 로그인을 한다.

해당 칸에 내 지갑 주소를 넣으면, 0.25 Ether를 무료로 받을 수 있다.
(테스트넷에서만 사용 가능한 가짜 돈이니까 좋아하지 말자)

Send Me ETH를 누르면,

무섭게 생긴 화면이 슝슝 내려온다. 좀 해킹 당하는 기분

이제 내 지갑에 접속해보면?

이렇게 테스트 ETH가 들어와있다!
나는 이미 몇번의 민팅을 해서 줄어들었지만, 처음이라면 아마 0.25 GoerliETH가 들어와있을 것이다.

(GOERLI FAUCET에서 받은 테스트 이더이기 때문에, 반드시 Goerli 테스트 네트워크로 전환해줘야한다. 상단의 버튼을 누르면 변경 가능)

2-2. 민팅하기

민팅 버튼을 눌러서 handleMint 이벤트를 발생시키면 메타마스크가 뜬다.

확인을 누르면 트랜잭션이 전송 요청이 전달되고,
조금의 시간이 지나면 트랜잭션 전송에 성공해서 receipt 콘솔 값이 나온다.

// recipt 값
{
	blockHash: "12345123451234123451234512345",
	from: "0x4Cccf230000000000000000000000",
	to: "스마트 컨트랙트 주소",
	transactionHash: "0x39064286d27b079869cee000000000000000000000",
	......
}

이제 내가 호출한 스마트 컨트랙트의 이더스캔에 접속해서, 트랜잭션이 잘 전달됐는지 보자!

2-3. 이더스캔 접속

알다시피 모든 트랜잭션은 공개적이다.
주소에 따른 트랜잭션 내역을 확인할 수 있게 해주는 대쉬보드 개념의 서비스가 바로 이더스캔.

Goerli Etherscan에 접속 후, 검색창에 내가 트랜잭션을 보낸 스마트 컨트랙트의 주소를 넣는다.

Transactions에는 트랜잭션 해시가 나와있는데, 내가 방금 보낸 트랜잭션 해시값이 Txn Hash로 나와있으니 해당 트랜잭션을 클릭해 Transaction Details로 들어간다.

내가 민팅한 기록이 남아있는데,
민팅이기 때문에 존재하지 않는 지갑에서 내 지갑으로 소유권이 옮겨졌다(Tokens Transferred).


이번 포스트에서는 민팅하고, 내 트랜잭션이 스마트 컨트랙트로 정상적으로 전송되는 것을 확인했다!

다음 포스트에서는,
유저 밸런스 조회 함수유저 소유 토큰 아이디 조회 함수를 이용한
내가 민팅해서 소유하고 있는 NFT를 표시해주는 기능을 만들어보자.

profile
Web Front-end developer

1개의 댓글

comment-user-thumbnail
2023년 3월 15일

안녕하세요.. 진짜 스마트 컨트랙트 연결에서 너무 곤혹을 겪고 있는데 작업하신 코드 참고하게 받을 수 있을까요..

답글 달기