💡 본 포스팅에서는
ethers js
를 사용하여 ERC-20approve
함수 (state-mutable 함수)를 호출하는 기본적인 방식을 담으려 합니다.
본 시리즈의 모든 내용은 ethers js v5를 기준으로 작성하였습니다.
작성일(2023.12.01) 기준, ethers js는 6.9.0 버전까지 공개되었고,
ethers js를 설치하면 기본으로 6 버전이 설치됩니다!
본문의 내용은 해당 버전과 상이할 수 있습니다.
본 포스팅에서 다룰 내용은, View 함수의 호출을 담았던 이전 포스팅과 상당 부분 유사하여, 다룰 내용이 많지는 않습니다!
ERC-20
표준의 approve
함수를 실행하는 과정을 따라가 보며, state-mutable 함수를 호출하는 과정을 간단히 짚어보겠습니다!
Signer
클래스는, 말 그대로 트랜잭션에 서명 및 실행을 통해
state-mutable 함수의 실행을 가능케 합니다.
Signer
클래스는 그 자체로는 추상 클래스이기 때문에,
Signer
클래스를 구현한 몇 가지 구현체 클래스들이 존재합니다.
이하에서는 Signer
의 대표적인 구현체 중 하나인 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
클래스에 대해서는 이전 포스팅에서 다루었기에, 바로 넘어가겠습니다!
대표적인 지갑 어플리케이션인 메타마스크
를 사용하시는 경우, 아래와 같은 순서로 개인 키를 조회합니다!
[ 1. Extension 우측 상단의 점 3개 => 계정 세부 정보 ]
[ 2. 개인 키 표시
클릭 후 비밀번호 입력 ]
하단 사각형 부분에 표시된 값이 비밀 키입니다.
위와 같이, 지갑의 비밀 키는 지갑의 소유자만이 접근할 수 있어,
웹서비스 개발 시에 백엔드 서버는 사용자의 Signer
클래스에 접근, 및 Signer
를 생성할 수 없습니다.
따라서, 일반적으로는 프론트 단에서 메타마스크 API 등을 통해
사용자의 지갑 클래스에 접근, 트랜잭션의 서명을 가능케 하는 방식을 사용하는 것 같습니다.
기본적으로는, View 함수 호출과 유사하게
호출할 함수를 ABI를 통해 인코딩한 후, 인코딩된 데이터를 data
필드에 넣어 전송하는 방식입니다.
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
하도록 로직을 작성하는 것이 좋습니다!
사실 1.에서 인코딩된 encodedData
와 to
주소만 가지고
아래와 같이 Wallet
클래스를 통해 호출해도 트랜잭션은 대체로 잘 동작합니다!
const tx = await wallet.sendTransaction({
to,
data:encoded,
})
하지만 보다 안전한 / 빠른 실행을 위해 gasPrice
와 gasLimit
을 함께 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. estimateGas
와 gasPrice
]
gasPrice
는 말 그대로 트랜잭션에서 사용할 가스의 가격을 추정하여 반환합니다.
estimateGas
는 이 트랜잭션이 어느 정도 수량의 가스를 사용할지를 추정하여 반환합니다.
따라서,
| ( estimateGas
의 반환값 ) * gasPrice
/ 1e18 = 가스비로 사용될 예상 nativeToken의 수량
정도가 될 것 같습니다!
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);
[ 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이면 성공을 의미합니다!