블록체인 뿅뿅

HardHat 튜토리얼 2편

bimppap 2023. 3. 9. 00:36

 

HardHat Tutorial 을 직접 번역해 봤습니다.
😎 더불어 개인적으로 유용했던 팁 또는 이해하는데 도움이 되는 개념들을 적어봤습니다.
>> 사용 스펙
MacOS
InteliJ IDE
Node.js
TypeScript
npm
Solidity
Polygon

 

이전 편은 아래 링크에서 확인할 수 있습니다.

2023.02.24 - [블록체인 뿅뿅] - HardHat 튜토리얼 1편

 

 

 

5. 컨트랙트 테스트하기

스마트 컨트랙트는 구동할 때마다 돈이 들기 때문에 반드시 돈이 들지 않는 테스트를 짜야 한다. 여기서는 컨트랙트를 테스트하기 위해 개발용 로컬 이더리움 네트워크인 hardhat를 사용한다. Hardhat에 내장되어 있어 기본 네트워크로 사용되며 따로 세팅을 할 필욘 없다. 테스트는 ethers.jsmocha를 사용해 구현할 것이다.

테스트 작성하기

새 디렉토리 test 를 생성한 후 디렉토리 내에 Token.test.ts 이라는 파일을 생성한다.

import { expect } from "chai";
import { ethers } from "hardhat"; // from "ethers"가 아닌 "hardhat"이다.

describe("Token contract", function () {
    it("Deployment should assign the total supply of tokens to the owner", async function() {
        // 이더리움 계정들을 가져온다.
        const [owner] = await ethers.getSigners() ;

        // ContractFactory는 새 컨트랙트를 배포하기 위해 쓰인다
        // 여기선 Token 컨트랙트 인스턴스 팩토리가 온다
        const Token = await ethers.getContractFactory("Token");

        // deploy는 ContractFactory에서 배포를 시작하고
        // 성공한 컨트랙트를 Promise 타입으로 반환한다.
        // connect()를 통해 배포 트랜잭션 발신자를 owner로 지정한다.
        // 발신자를 특정 주소로 두지 않을 경우 connect()는 생략할 수 있다.
        // (owner가 default) 
        const tokenInstance = await Token.connect(owner).deploy();

        // 배포된 후에 토큰의 컨트랙 메서드를 부를 수 있다
        const ownerBalance = await tokenInstance.balanceOf(owner.address);
        // 토큰 수량과 (토큰을 배포한)계정이 가진 수량을 비교한다
        // Chai 모듈을 활용하고 있다.
        //(@nomicfoundation/hardhat-chai-mathchers를 사용하며 toolbox에 포함되어 있다.)
        expect(await tokenInstance.totalSupply()).to.equal(ownerBalance);
    });
});

이후 터미널에서 npx hardhat test 를 돌려보면 아래와 같은 결과가 나올 것이다.

 

 

(+) inteliJ IDE를 활용해 테스트를 하고 싶다면 아래를 참고하자.

더보기

intelijIDE에서 mocha + typescript로 단일 테스트 돌리는 방법

방법1) 터미널 수동 입력

 npx hardhat test —grep "단어를 쓰면 단어가 포함된 테스트들을 돌린다"

방법2) 테스트 구성 편집

구성 편집 클릭
왼쪽 하단 아래 구성 템플릿 편집 클릭
mocha에서 환경변수랑 추가 Mocha 옵션 위와 같이 변경


끝!

 

여러 계정을 가지고 테스트 하기

Contract 객체에서 connect() 메서드를 이용해 트랜잭션을 수행할 유저를 지정할 수 있다.

import { expect } from "chai";

describe("Token contract", function () {
  // ...previous test...

  it("Should transfer tokens between accounts", async function() {
    const [owner, addr1, addr2] = await ethers.getSigners();

    const Token = await ethers.getContractFactory("Token");

    const hardhatToken = await Token.deploy();

    // 오너가 addr1에게 토큰 50개를 보낸다
    await hardhatToken.transfer(addr1.address, 50);
    expect(await hardhatToken.balanceOf(addr1.address)).to.equal(50);

    // addr1이 addr2에게 토큰 50개를 보낸다 (connect 메서드 사용)
    await hardhatToken.connect(addr1).transfer(addr2.address, 50);
    expect(await hardhatToken.balanceOf(addr2.address)).to.equal(50);
  });
});

상수 사용하기

describe 와 it 사이에 상수를 선언하여 여러 it 테스트들에서 공용으로 쓰이게 할 수 있다. 이를 통해 코드 중복을 피하고 성능을 향상시킬 수 있다.

describe("Token contract", function () {
    async function deployTokenFixture() {
        const Token = await ethers.getContractFactory("Token");
        const [owner, addr1, addr2] = await ethers.getSigners();

        const hardhatToken = await Token.deploy();
        await hardhatToken.deployed();

        return {Token, hardhatToken, owner, addr1, addr2};
    }

    describe("Deployment", function () {
        it("Should set the right owner", async function() {
            const {hardhatToken, owner} = await loadFixture(deployTokenFixture);
            expect(await hardhatToken.owner()).to.equal(owner.address);
        });

        it("Should assign the total supply of tokens to the owner", async function() {
            const {hardhatToken, owner} = await loadFixture(deployTokenFixture);
            const ownerBalance = await hardhatToken.balanceOf(owner.address);
            expect(await hardhatToken.totalSupply()).to.equal(ownerBalance);
        });
    });

    describe("Transactions", function () {
        it("Should transfer tokens between accounts", async function() {
            const {hardhatToken, owner, addr1, addr2} = await loadFixture(deployTokenFixture);

            await expect(
                hardhatToken.transfer(addr1.address, 50)
            ).to.changeTokenBalances(hardhatToken, [owner, addr1], [-50, 50]);

            await expect(
                hardhatToken.connect(addr1).transfer(addr2.address, 50)
            ).to.changeTokenBalances(hardhatToken, [addr1, addr2], [-50, 50]);
        });

        it("Should emit Transfer events", async function() {
            const {hardhatToken, owner, addr1, addr2} = await loadFixture(deployTokenFixture);

            await expect(hardhatToken.transfer(addr1.address, 50))
                .to.emit(hardhatToken, "Transfer")
                .withArgs(owner.address, addr1.address, 50);

            await expect(hardhatToken.connect(addr1).transfer(addr2.address, 50))
                .to.emit(hardhatToken, "Transfer")
                .withArgs(addr1.address, addr2.address, 50);
        });

        it('should fail if sender do not have enough tokens', async function () {
            const {hardhatToken, owner, addr1} = await loadFixture(deployTokenFixture);
            const initialOwnerBalance = await hardhatToken.balanceOf(owner.address);

            await expect(
                hardhatToken.connect(addr1).transfer(owner.address,1))
                .to.be.revertedWith("Not enough tokens");

            expect(await hardhatToken.balanceOf(owner.address)).to.equal(initialOwnerBalance);
        });
    });
});

 

커버리지 확인하기

관리해야 할 컨트랙트가 많아지고 코드량이 증가하면 테스트 되지 않은 코드를 발견하기 어려워질 수도 있다. 이때 터미널에서 npx hardhat coverage 를 돌려보면 퍼센트 단위로 커버리지 범위를 확인할 수 있다.

 

coverage를 돌리면 루트 경로에 coverage 파일이 생긴다. coverage 안의 index.html을 로컬에서 열어보면 더 자세한 정보를 확인할 수 있다.

 

6. HardHat 네트워크를 통해 디버깅하기

Hardhat은 개발용으로 설계된 로컬 이더리움 네트워크인 Hardhat 네트워크와 함께 제공된다. 이를 통해 로컬 내에서 스마트 컨트랙트를 배포하고, 테스트를 실행하고, 코드를 디버그할 수 있게 된다. 따로 세팅할 건 없고 바로 테스트를 실행하면 된다.

Hardhat 네트워크에서 컨트랙트, 테스트를 실행할 때 console.log() 를 이용해 Solidity 코드에서 쓰이는 로깅 메시지 및 변수를 확인할 수 있다. 아래와 같이 import 하면 사용할 수 있다.

pragma solidity ^0.8.9;

import "hardhat/console.sol";

contract Token {
  //...
}

이후 아래와 같이 사용하면 된다.

function transfer(address to, uint256 amount) external {
    require(balances[msg.sender] >= amount, "Not enough tokens");

		// 추가된 코드
    console.log(
        "Transferring from %s to %s %s tokens",
        msg.sender,
        to,
        amount
    );

    balances[msg.sender] -= amount;
    balances[to] += amount;

    emit Transfer(msg.sender, to, amount);
}

테스트를 돌리면 아래와 같이 나온다.

 

7. 테스트넷에 배포하기

dApp을 다른 사람과 공유할 준비가 되면 라이브 네트워크에 배포할 수 있다. 라이브 네트워크에 배포하면 다른 사용자가 컨트랙트에 접근할 수 있다.

"메인넷" 네트워크는 실제 돈을 다루지만 그렇지 않는 별도의 "테스트넷" 네트워크도 있다. 이 테스트넷은 실제 돈을 걸지 않고 실제 시나리오를 모방하는 환경을 제공한다. 소프트웨어 수준에서 테스트넷에 배포하는 것은 메인넷에 배포하는 것과 동일하다. 유일한 차이점은 연결하는 네트워크 뿐이다. 이더리움의 경우 goerli, sepolia가 있고 폴리곤의 경우 mumbai가 있다.

 

새 디렉토리 scripts 를 생성한 후 디렉토리 내에 deploy.script.ts 이라는 파일을 생성한다.

// deploy.script.ts
import {ethers} from "hardhat";

async function main() {
    try {
        const [deployer] = await ethers.getSigners();

        console.log("Deploying contracts with the account:", deployer.address);
        console.log("Account balance: ", (await deployer.getBalance()).toString());

        const Token = await ethers.getContractFactory("Token");
        const token = await Token.deploy();

        console.log("Token address:", token.address)

    } catch (e) {
        console.error(e);
    }
}

void main();

HardHat을 이용해 특정 Ethereum 네트워크에 연결하려면 다음과 같이 작업을 실행할 때 --network 매개변수를 사용한다. 매개변수 없이 실행하면 로컬 hardhat 네트워크가 디폴트로 돌아간다. Hardhat에서는 실행이 끝나면 배포된 컨트랙트가 사라지기 때문에 테스트하기에 유용하다.

npx hardhat run scripts/deploy.script.ts --network <network-name>

원격 네트워크에 배포하기

메인넷 또는 테스트넷과 같은 원격 네트워크에 배포하려면 파일에 network 항목을 추가해야 한다.

튜토리얼에서는 Goerli 라는 이더리움 테스트넷을 쓰지만 개인적으로 Mumbai 라는 폴리곤 테스트넷이 더 빨라 Mumbai를 추천한다.

import "@nomicfoundation/hardhat-toolbox";
import '@typechain/hardhat';

import { HardhatUserConfig } from 'hardhat/config';

// Alchemy에서 api 키를 받아 기입한다. (아래 참고)
const ALCHEMY_MUMBAI_API_KEY = "KEY";

// 메타마스크를 이용해 지갑 개인 키를 기입한다. (아래 참고)
const PRIVATE_KEY = "YOUR PRIVATE KEY";

const config: HardhatUserConfig = {
  solidity: "0.8.17",
  networks: {
    mumbai: {
      url: `https://polygon-mumbai.g.alchemy.com/v2/${ALCHEMY_MUMBAI_API_KEY}`,
      accounts: [PRIVATE_KEY]
    }
  },
  // ...
};

export default config;

(+) Alchemy Key 받는 법

alchemy 대신 infura 같은 다른 노드나 게이트웨이를 써도 되지만 여기선 alchemy를 사용하고 있다.

alchemy 홈페이지로 들어가 회원가입을 한다.

로그인 되면 대쉬보드에 들어가게 된다.

CREATE APP를 눌러 아래와 같이 작성한다. (Name은 다르게 작성해도 된다.)

다시 대시보드로 돌아와 제일 우측에 있는 VIEW KEY를 클릭하면 API KEY가 나온다.

 

(+) 메타마스크 개인키 받는 법

크롬 확장 또는 앱에서 더보기 버튼 - 계정 세부 정보를 클릭한다.

팝업이 뜨면 비공개 키 내보내기 클릭 - 메타마스크 패스워드 입력 - 비공개 키 복사

비공개키를 공유하면 지갑이 해킹당할 수 있기 때문에 절대 공유하면 안된다!!

 

mumbai에 배포하려면 배포할 주소에 mumbai matic을 보내줘야 한다. Mumbai Faucet 사이트에서 테스트넷 matic를 얻을 수 있습니다.

npx hardhat run scripts/deploy.script.ts --network mumbai

트랜잭션은 폴리곤 스캔에서 확인할 수 있다. (링크)

 

8.  보일러플레이트 프로젝트 사용해보기

https://github.com/NomicFoundation/hardhat-boilerplate

위 레포를 클론한 후 아래 명령어를 실행하면 로컬에 hardhat 네트워크가 돌아간다. 웹 앱을 사용하는 동안 해당 터미널 창은 끄면 안된다.

cd hardhat-boilerplate
npm install
npx hardhat node

새 터미널 창을 열어 hardhat 네트워크에 컨트랙트를 배포한다.

npx hardhat --network localhost run scripts/deploy.js

이후 같은 창에서 웹 앱을 실행한다.

cd frontend
npm install
npm run start

인터넷을 키고 localhost:3000 으로 가면 아래 화면이 뜬다.

이 때, 메타마스크 네트워크는 127.0.0.1:8545에 연결되어 있어야 한다. 추가는 아래와 같이 하면 된다.

이후 버튼을 클릭하면 아래와 같은 창이 뜬다.

아래 명령어를 치면 hardhat 에서 입력한 계정으로 1 ETH과 100MHT를 전송해준다.

npx hardhat --network localhost faucet <your address>

9. 결론

Hardhat 튜토리얼을 끝마쳤다. (야호!)

HardHat 튜토리얼에서는 도움이 될 링크들을 적어놓았다.