이전 포스트에서는 지갑 연결하기를 해봤다.
이 포스트에서는 대망의 NFT 민팅을 해볼 것이다!
민팅으로 어떻게 트랜잭션을 발생시키고,
그 트랜잭션은 실제 컨트랙트 상에서 어떻게 처리되는지 직접 눈으로 보면서 흐름을 이해해보자.
- 스마트 컨트랙트 민팅 로직
이미 배포되어있는 스마트 컨트랙트 상에 토큰 아이디는 순차적으로 증가하고,
민트 버튼을 누른 순간 해당 번째의 토큰 아이디를 받아온다는 로직이 짜여져있다는 전제를 두고 진행한다.- 프로젝트 구현 방향
토큰 아이디는 미리 알 수 없고, 내가 민팅을 해서 소유권을 받는 순간 해당 토큰 아이디가 표시된다.
민팅을 하기 위해서는 반드시 아래의 두가지가 필요하다.
주소
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 공식문서 발췌
contract 인스턴스가 가지고 있는 populateTransaction
메소드와 sendTransaction
메소드를 사용한다.
전송할 트랜잭션 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의 값은 트랜잭션의 인풋 데이터!
(이더스캔의 트랜잭션 기록에서 확인 가능)
네트워크에 트랜잭션을 전송하는 메소드.
그런데 트랜잭션을 전송하려면?
데이터 뿐만 아니라, 디지털 서명
도 반드시 들어가야한다.
📌 트랜잭션 구조에 대한 자세한 설명은 블록체인(Block Chain) - 트랜잭션(Transaction)에서
sendTransaction로 트랜잭션을 전송하고 싶은데,
트랜잭션을 일으키기 위해서는 반드시 서명
을 해야한다.
✨ 트랜잭션에 서명(Sign)하는 방법 🔏
- 잠금해제된 계정으로 서명하기 (Using unlocked accounts)
:truffle-hdwallet-provider
같은 전문 Web3 공급자( =지갑 provider. 우리는메타마스크의 provider
를 사용 중 )를 사용하여 잠금 해제된 계정을 생성할 수도 있다.
- 개인키로 직접 서명하기 (Using a private key)
: 트랜잭션을 시작하는 주소의 개인 키를 사용하여 트랜잭션에 서명할 수 있다. 하지만 개인 키를 다루는 건 매우 까다롭고 주의가 필요하다.
📌 이게 바로 세상을 바꿀 Web3 - web3.js란? 에서 설명한,
메타마스크가 제공하는 web3로 Web3 Provider를 초기화하는 이유 !!!!!
( How do I sign transactions with Web3? 참고 )
useWeb3React
의 library
에는 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);
지금 web3.js를 사용하고 있으니 web3가 제공하는 컨트랙트 인스턴스 생성방식으로 하면 좋겠지만,
web3안에 Contract라는 메소드를 찾을 수 없어서 우선 1-2처럼 ethers가 제공하는 Contract 메소드를 사용해야겠다.
yarn add ethers
ethers와 web3를 모두 설치할 필요가 없을텐데, 왜 둘다 필요한걸까?
나는 이미 배포되어있는 컨트랙트가 있기 때문에, 해당 컨트랙트의 주소와 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);
};
이렇게 컨트랙트의 인스턴스가 생성됐다.
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;
이렇게 민팅 로직을 완료했다.
이제 어떻게 민팅이 이루어지는지 눈으로 보자!
작업환경은 testnet인 goerli
테스트 이더를 받아오기 위해 GOERLI FAUCET에 접속한 뒤, 간단하게 구글로 로그인을 한다.
해당 칸에 내 지갑 주소를 넣으면, 0.25 Ether를 무료로 받을 수 있다.
(테스트넷에서만 사용 가능한 가짜 돈이니까 좋아하지 말자)
Send Me ETH를 누르면,
무섭게 생긴 화면이 슝슝 내려온다. 좀 해킹 당하는 기분
이제 내 지갑에 접속해보면?
이렇게 테스트 ETH가 들어와있다!
나는 이미 몇번의 민팅을 해서 줄어들었지만, 처음이라면 아마 0.25 GoerliETH가 들어와있을 것이다.
(GOERLI FAUCET에서 받은 테스트 이더이기 때문에, 반드시 Goerli 테스트 네트워크로 전환해줘야한다. 상단의 버튼을 누르면 변경 가능)
민팅 버튼을 눌러서 handleMint
이벤트를 발생시키면 메타마스크가 뜬다.
확인을 누르면 트랜잭션이 전송 요청이 전달되고,
조금의 시간이 지나면 트랜잭션 전송에 성공해서 receipt 콘솔 값이 나온다.
// recipt 값
{
blockHash: "12345123451234123451234512345",
from: "0x4Cccf230000000000000000000000",
to: "스마트 컨트랙트 주소",
transactionHash: "0x39064286d27b079869cee000000000000000000000",
......
}
이제 내가 호출한 스마트 컨트랙트의 이더스캔에 접속해서, 트랜잭션이 잘 전달됐는지 보자!
알다시피 모든 트랜잭션은 공개적이다.
주소에 따른 트랜잭션 내역을 확인할 수 있게 해주는 대쉬보드 개념의 서비스가 바로 이더스캔.
Goerli Etherscan에 접속 후, 검색창에 내가 트랜잭션을 보낸 스마트 컨트랙트의 주소를 넣는다.
Transactions
에는 트랜잭션 해시가 나와있는데, 내가 방금 보낸 트랜잭션 해시값이 Txn Hash
로 나와있으니 해당 트랜잭션을 클릭해 Transaction Details로 들어간다.
내가 민팅한 기록이 남아있는데,
민팅이기 때문에 존재하지 않는 지갑에서 내 지갑으로 소유권이 옮겨졌다(Tokens Transferred
).
이번 포스트에서는 민팅
하고, 내 트랜잭션이 스마트 컨트랙트로 정상적으로 전송되는 것을 확인했다!
다음 포스트에서는,
유저 밸런스 조회 함수
와 유저 소유 토큰 아이디 조회 함수
를 이용한
내가 민팅해서 소유하고 있는 NFT를 표시해주는 기능을 만들어보자.
안녕하세요.. 진짜 스마트 컨트랙트 연결에서 너무 곤혹을 겪고 있는데 작업하신 코드 참고하게 받을 수 있을까요..