! 주의 / 위에서 사용된 주소로 암호화폐를 전송할 시 영원히 사라질 수 있으니 사용하지 말 것을 당부드립니다.
실제로 클라이언트에서 어떻게 컨트랙트와 상호작용할 수 있는지 살펴보려고 한다. 특히 이번에는 이더리움 기반 지갑으로 유명한 메타마스크를 사용해봤다. 메타마스크를 통해서 로컬 이더리움 테스트넷의 주소를 가져와 사용할 수 있어서 더욱더 클라이언트 사용 시나리오에 근접할 수 있게 되었다.
이미 많은 테스트넷에 구현되어있는 Faucet 스마트 컨트랙트를 배포하고, 클라이언트가 해당 컨트랙트에 암호화폐를 기부하거나 가져와서 쓸 수 있는 웹 프로그램이다.
React
클라이언트에 web3.js
라이브러리를 사용했다. 스마트 컨트랙트 작성은 solidity
를 이용하였다. 테스트넷은 ganache
를 통해 생성했고, 컨트랙트 배포 및 테스트넷 접근은 truffle
을 이용하였다.
// SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.6.0 <=0.9.0;
contract Faucet {
receive() external payable{}
function withdraw(uint withdraw_amount) public {
require(withdraw_amount <= 1 ether);
payable(msg.sender).transfer(withdraw_amount);
}
}
위에 작성된 컨트랙트를 ganache
를 통해 생성된 테스트넷에 truffle
을 이용하여 배포해준다. 그리고 truffle
을 이용하였다면 build 폴더 -> contracts 폴더에 배포한 컨트랙트의 json 파일이 생성된다. 여기에는 만든 컨트랙트 관련한 여러 데이터가 저장되는데, 그 중에는 배포된 컨트랙트의 API 격인 ABI가 있으니 잘 챙겨둬야 한다.
ganache
로 만든 테스트넷에 메타마스크를 연동해야한다. 또한 ganache
테스트넷이 만들어질 때 생성된 계정 주소 하나를 메타마스크 지갑에 가져와 사용하자. truffle
에서 컨트랙트를 작성하거나 트랜잭션을 보낼 때 기본값으로 설정되어 있는 제일 첫 번째 account 주소를 가져오면 편하다. 메타마스크에 등록할 때는 해당 주소의 개인키를 대신해서 기입해주면 된다.
[메타마스크 처음 모습]
[ganache 로컬 네트워크 적용]
[계정 가져오기]
[등록된 계정 모습]
import {useEffect, useState} from 'react';
import Web3 from "web3/dist/web3.min.js";
const useWeb3 = ()=>{
const [account, setAccount] = useState(null);
const [web3, setWeb3] = useState(null);
useEffect(()=>{
(async ()=>{
if (!window.ethereum) return;
const [address] = await window.ethereum.request({
method: 'eth_requestAccounts',
})
setAccount(address);
const web3 = new Web3('ws://localhost:8545');
setWeb3(web3);
})();
}, []);
return [web3, account];
};
export default useWeb3;
위 코드는 미리 web3를 받아오는 작업을 하는 함수 useWeb3
를 정의한다. 사실 React의 Hook을 모티브로 구현한 것 같다. window.ethereum
의 갑작스러운 등장에 당황스러울텐데, 이는 브라우저에서 작동하고 있는 메타마스크가 제공해준다. request가 실행되면 메타마스크에서 해당 웹페이지와 연동을 할 건지, 브라우저 어플이 확인을 한다. 현재 메타마스크에 연동되어있는 주소값을 요청했기에, 문제 없으면 메타마스크에 등록했던 주소값을 가져올 수 있다.
import './App.css';
import useWeb3 from "./hooks/useWeb3";
import {useState, useEffect} from "react";
import Faucet from "./contracts/Faucet.json";
function App() {
const [web3, account] = useWeb3();
const [faucet, setFaucet] = useState(null);
const [number, setNumber] = useState(0);
const contractAddress = "Faucet의 배포된 주소"
const giveBtnClickHandler = async ()=>{
if(number > 0){
const result = await web3.eth.sendTransaction({from:account, to:contractAddress, value:number});
console.log(result);
}
}
const receiveBtnClickHandler = async ()=>{
if(number > 0){
const result = await faucet.methods.withdraw(number).send({from:account});
console.log(result);
}
}
const numChangeHandler = (e)=>{
setNumber(e.target.value);
}
useEffect(()=>{
if (!web3) return;
const contract = new web3.eth.Contract(Faucet.abi, contractAddress)
setFaucet(contract);
}, [web3])
return (
<div className="App">
<div className="container">
<div className="box">
<div>
<p>내 주소</p>
{ account ? <p>{account}</p>
: <p>메타마스크와 연결이 안되었습니다.</p>
}
</div>
<div className="input-box">
<input placeholder="숫자를 입력하세요 (1 wei)" type="number" value={number} onChange={numChangeHandler}/>
</div>
<button type="button" className="btn" onClick={giveBtnClickHandler}>Give Ether</button>
<button type="button" className="btn" onClick={receiveBtnClickHandler}>Receive Ether</button>
</div>
</div>
</div>
);
}
export default App;
먼저 useEffect
부분을 확인해보자. 거기서 배포된 컨트랙트와의 접점을 만들어주는 작업을 한다. web3.eth.Contract
는 마치 네트워크에 배포된 컨트랙트를 클라이언트에서 객체처럼 사용할 수 있게 도와준다. 재료는 아까 중요하다고 얘기했던 스마트 컨트랙트 배포 후 만들어진 json 파일과 네트워크의 해당 컨트랙트의 주소값이다. 여기서 연결한 Faucet 컨트랙트는 App 컴포넌트의 상태값 faucet에 할당된다.
receiveBtnClickHandler
를 살펴보면 facuet를 통해 컨트랙트의 함수에 접근, 실행시키고 있는 모습을 확인할 수 있다.
전부 이해하진 못했지만 ganache, truffle의 쓰임새와 웹앱에서 메타마스크 연동하는 방법 참고해보고 갑니다.