[Ethers Js 겉핥기] #3. state-mutable 함수의 호출

toto9602·2023년 12월 1일
0

Ethers js

목록 보기
4/5

💡 본 포스팅에서는 ethers js를 사용하여 ERC-20 approve 함수 (state-mutable 함수)를 호출하는 기본적인 방식을 담으려 합니다.

본 시리즈의 모든 내용은 ethers js v5를 기준으로 작성하였습니다.

작성일(2023.12.01) 기준, ethers js는 6.9.0 버전까지 공개되었고,
ethers js를 설치하면 기본으로 6 버전이 설치됩니다!
본문의 내용은 해당 버전과 상이할 수 있습니다.

참고 자료

ethers js 공색 문서
Metamask Provider API

state-mutable 함수 호출하기

본 포스팅에서 다룰 내용은, View 함수의 호출을 담았던 이전 포스팅과 상당 부분 유사하여, 다룰 내용이 많지는 않습니다!

ERC-20 표준의 approve 함수를 실행하는 과정을 따라가 보며, state-mutable 함수를 호출하는 과정을 간단히 짚어보겠습니다!

Signer 클래스 생성하기

ethers js 공식 문서 설명

Signer 클래스는, 말 그대로 트랜잭션에 서명 및 실행을 통해
state-mutable 함수의 실행을 가능케 합니다.

Signer 클래스는 그 자체로는 추상 클래스이기 때문에,
Signer 클래스를 구현한 몇 가지 구현체 클래스들이 존재합니다.

이하에서는 Signer 의 대표적인 구현체 중 하나인 Wallet 클래스를 이용하여 진행하겠습니다!

Wallet 클래스 생성

ethers 공식문서에 따르면, Wallet 클래스의 생성자는 사용자 지갑의 개인 키와, Optinal한 필드로 Provider 클래스를 받습니다!

const rpcUrl = `https://eth.llamarpc.com`; // Ethereum public node RPC URL 

// Wallet 클래스 생성
const wallet = new Wallet(`${PRIVATE_KEY}`, new ethers.providers.JsonRpcProvider(rpcUrl) 

Provider 클래스에 대해서는 이전 포스팅에서 다루었기에, 바로 넘어가겠습니다!

cf. 개인키 조회 ( feat. 메타마스크 )

대표적인 지갑 어플리케이션인 메타마스크를 사용하시는 경우, 아래와 같은 순서로 개인 키를 조회합니다!

[ 1. Extension 우측 상단의 점 3개 => 계정 세부 정보 ]

[ 2. 개인 키 표시 클릭 후 비밀번호 입력 ]
하단 사각형 부분에 표시된 값이 비밀 키입니다.

cf. 메타마스크 Library

위와 같이, 지갑의 비밀 키는 지갑의 소유자만이 접근할 수 있어,
웹서비스 개발 시에 백엔드 서버는 사용자의 Signer 클래스에 접근, 및 Signer를 생성할 수 없습니다.

따라서, 일반적으로는 프론트 단에서 메타마스크 API 등을 통해
사용자의 지갑 클래스에 접근, 트랜잭션의 서명을 가능케 하는 방식을 사용하는 것 같습니다.

[ cf. 메타마스크 Provider API ]

sendTransaction 호출

기본적으로는, View 함수 호출과 유사하게
호출할 함수를 ABI를 통해 인코딩한 후, 인코딩된 데이터를 data 필드에 넣어 전송하는 방식입니다.

1. 트랜잭션 인코딩

const APPROVE_INTERFACE = [
  "function approve(address spender, uint256 amount) external returns (bool)"
]

// Interface 클래스를 생성합니다. 
const iface = new ethers.utils.Interface(APPROVE_INTERFACE);

// 첫 번째 인자로 함수명, 2번째 인자로 호출할 함수의 매개변수를 배열로 넣어 인코딩합니다. 
const encodedData = iface.encodeFunctionData("approve", [`${SPENDER_ADDRESS}`, ethers.constants.MaxUint256];

View 함수 호출 때와 동일하게 진행하는 부분입니다!
위 예시에는 amount 필드를 ethers.constants.MaxUint256 을 사용하여 가능한 최대 수량을 허용하지만,
실제 개발 시에는 그때그때 필요한 수량만큼을 approve 하도록 로직을 작성하는 것이 좋습니다!

2. gasPrice, gasLimit 조회

사실 1.에서 인코딩된 encodedDatato 주소만 가지고
아래와 같이 Wallet 클래스를 통해 호출해도 트랜잭션은 대체로 잘 동작합니다!

  const tx = await wallet.sendTransaction({
    to,
    data:encoded,
  })

하지만 보다 안전한 / 빠른 실행을 위해 gasPricegasLimit 을 함께 sendTransaction에 보내 보겠습니다!

import * as MathJS from "mathjs";

const to = `${트랜잭션을 보낼 주소}`;

// 네트워크의 gasPrice를 조회합니다. 
const gasPrice = await wallet.getGasPrice();

// 트랜잭션 실행에 필요한 것으로 예상되는 gas의 양을 조회합니다.
const estimatedGas = await wallet.estimateGas({to:to, data:encodedData})

// 예상되는 수량에, 버퍼 차원에서 일정 수량을 더해 주겠습니다..!
const gasLimit = estimatedGas.add(Bignumber.from(`${BUFFER_AMOUNT}`));

[ cf. estimateGasgasPrice ]

gasPrice 는 말 그대로 트랜잭션에서 사용할 가스의 가격을 추정하여 반환합니다.

estimateGas는 이 트랜잭션이 어느 정도 수량의 가스를 사용할지를 추정하여 반환합니다.

따라서,
| ( estimateGas 의 반환값 ) * gasPrice / 1e18 = 가스비로 사용될 예상 nativeToken의 수량

정도가 될 것 같습니다!

3. 실행하기

const rpcUrl = `https://polygon.llamarpc.com"`;

const wallet = new Wallet(`${PRIVATE_KEY}`, new ethers.providers.JsonRpcProvider(rpcUrl);

const encodedData = new ethers.utils.Interface(APPROVE_INTERFACE).encodeFunctionData("approve", [`${SPENDER_ADDRESS}`, ethers.constants.MaxUint256]);

const to = `${TO_ADDRESS}`

const estimatedGas = await wallet.estimateGas({to, data:encodedData})
const gasPrice = await wallet.getGasPrice();


const tx = await wallet.sendTransaction({
  to, 
  data:encodedData,
  gasPrice,
  gasLimit:estimatedGas.add(BigNumber.from("100000"))
});

console.log(tx);

4. 결과 확인하기

[ 1. Explorer 조회 ]

sendTransaction의 결과를 console.log로 찍어 보면,
TransactionResponse 객체가 반환됩니다.

{
	type: number,
    chainId: number,
    nonce: number,
    maxPriorityPerGas: Bignumber,
    maxFeePerGas: Bignumber,
    gasPrice: Bignumber,
    gasLimit: Bignumber,
    to: string, 
    value: Bignumber,
    data: string,
    accessList: AccessList,
    hash: string,
    v: number,
    r: string,
    s: string,
    from: string,
    confirmations: number,
    wait: [Function (anonymous)]

이 중 hash 값을, 네트워크별로 존재하는 Explorer에 조회하면 결과 확인이 가능합니다!
저는 Polygon 네트워크에서 실행했으므로, Polygonscan에서 확인하겠습니다.

Success 로, 성공한 걸 확인할 수 있네요!

[ 2. TransactionReceipt 조회 ]

const provider = new ethers.providers.JsonRpcProvider(rpcUrl);

const txReceipt = await providr.getTransactionReceipt(txHash);

const SUCCESS_STATUS = 1;
const REVERT_STATUS = 0;

if (txReceipt.status === SUCCESS_STATUS) {
	console.log(`트랜잭션 성공!!`)};

if (txReceipt.status === REVERT_STATUS) {
	console.log(`트랜잭션 실패ㅠㅠ`);
}

위와 같이, Provider 클래스를 통해 조회한 transactionReceipt 객체로도 실행 상태를 확인할 수 있습니다!

status 필드가 0이면 실패를, 1이면 성공을 의미합니다!

[ cf. TransactionReceipt 공식 문서 설명 ]

profile
주니어 백엔드 개발자입니다! 조용한 시간에 읽고 쓰는 것을 좋아합니다 :)

0개의 댓글