이번 포스팅에서는 이더리움 클라이언트에게 요청을 보낼 때 사용하는 라이브러리인 Web3 라이브러리에 대해 다뤄보고자 한다. Web3 라이브러리를 사용하기에 앞서 블록체인 네트워크 상에서 노드 간에 통신을 할 때 사용하는 RPC에 대해 먼저 알아보도록 하자.
< 목차 >
- RPC
- Web3 라이브러리
- Web3 기본 메소드 정리
1. RPC
RPC 란, 원격 프로시저 호출 (Remote Procedure Call) 을 일컫는 말로 별도의 원격 제어를 위한 코딩 없이 다른 주소 공간에서 함수나 프로시저를 실행할 수 있게 하는 프로세스 간 통신 기술이다. 일반적으로 프로세스(process)는 자신의 주소 공간 안에 존재하는 함수만 호출하여 실행 가능하다. 하지만 RPC를 사용할 경우 자신과 다른 주소 공간에서 동작하는 프로세스의 함수를 실행할 수 있게 된다. 다시 말해, RPC를 이용하면 프로그래머는 함수가 실행 프로그램의 로컬 위치에 있든 원격 위치에 있든 동일한 코드를 이용할 수 있게된다.
RPC를 사용하면 분산 네트워크 환경에서 프로그래밍을 할 때 프로세스 간 통신 기능을 비교적 쉽게 구현할 수 있고 정교한 제어 또한 가능해진다. 그리고 고유 프로세스 개발에만 집중할 수 있다는 장점도 갖고 있다.
이더리움 네트워크에서는 이더리움 블록체인 네트워크를 구성하는 개별 클라이언트 노드를 "이더리움 클라이언트" 라고 부른다. 그리고 이더리움 재단이 재공하는 클라이언트 소프트웨어인 게스(Geth)에서는 RPC를 통해 상호작용 할 수 있는 api를 제공하고 있다. 따라서 RPC에 대한 이해와 어떤식으로 요청과 응답이 이루어지는지에 대해 알고 넘어가야할 필요성이 있다.
지난 포스팅에서 가나쉬(ganache)를 설치해 로컬 이더리움 네트워크를 생성해 보았다. 이번에는 가나쉬로 생성한 로컬 이더리움 네트워크에 RPC 요청을 보내보면서 WSL 환경에서 curl을 이용해 RPC 요청을 보내는 방법에 대해 알아보고자 한다.
참고)
2022.06.28 - [Ethereum] - Ethereum/이더리움 - 개발 환경 세팅
우선 터미널에서 npx ganache-cli 명령어를 입력해 로컬 이더리움 네트워크를 생성해 주도록 하자. 터미널 창에 Listening on 127.0.0.1:8545 와 같은 내용이 출력되었다면 로컬 이더리움 네트워크가 문제없이 생성된 것이다.
$ npx ganache-cli
curl 을 사용해 요청을 보내는 방법에 대해 간략히 짚고 넘어가자. 아래와 같이 curl 명령어와 함께 "-X 요청 메소드 -H 요청 헤더 --data 요청 바디" 를 입력해 cli 로 요청을 보내는 것이 가능하다.
$ curl -X POST -H "content-type:application/json" --data '{name: "bitkunst"}' http://localhost:3000
- -X : 요청 메소드 작성
- -H : 요청 헤더 내용 작성
- --data / -d : 요청 바디 내용 작성
이제 가나쉬로 생성한 이더리움 클라이언트에 curl을 이용해 RPC 요청을 보내보도록 하자.
이더리움 클라이언트에 요청을 보낼 때는 다음과 같은 형식으로 요청을 보내줘야만 한다.
- request method : POST
- request header option : "Content-type: application/json"
- request body
{
"id": 1337, // 체인 아이디, 선택
"jsonrpc": "2.0", // 필수
"method": "eth_accounts", // 필수
"params": [] // 메소드의 인자값
}
request body에 들어가게 되는 데이터는 위와 같은 형식을 갖는데 각각의 속성은 다음과 같은 값을 갖는다.
- id : 체인 아이디
- 이더리움 메인넷 : 1
- 이더리움 테스트넷 : 2, 3, 4, 5
- 가나쉬가 제공하는 네트워크의 체인 아이디 : 1337
- jsonrpc : JSON으로 인코딩된 원격 프로시저 호출
- method : 이더리움 클라이언트에서 구현되어 있는 메소드명
- params : 메소드의 인자값
👉 계정 가져오기
$ curl -X POST \
-H "Content-type: application/json" \
--data '{ "jsonrpc": "2.0", "method": "eth_accounts", "params": [] }' \
http://localhost:8545
이 때 이더리움 네트워크 내에 존재하는 모든 계정을 가져오는 것이 아닌, 해당 노드가 관리하는 계정만을 가져온다. ( 가령 A → B 노드로 요청을 보냈다면 B 노드에 존재하는 계정들을 가져온다. )
👉 잔액 조회하기
$ curl -X POST \
-H "Content-type: application/json" \
--data '{ "jsonrpc": "2.0", "method": "eth_getBalance", "params": ["0xCE3c05be302b50314C1f82116771f88Cd6C0dD4E", "latest"] }' \
http://localhost:8545
특정 계정의 잔액을 조회하는 메소드명은 "eth_getBalance" 이며 params 속성값으로 2개의 인자값을 전달한다. 필수적으로 들어가야 하는 인자값은 잔액을 조회할 계정 주소(address) 이며 몇번째 블록에서 조회할 것인지에 대한 Block number를 두번째 인자값으로 넣어준다.
위에서 언급한 메소드 이외에도 RPC를 통해 상호작용 할 수 있는 다양한 api를 제공하고 있으므로 이더리움 gitHub 페이지에서 확인해보길 바란다.
https://ethereum.github.io/execution-apis/api-documentation/
2. Web3 라이브러리
앞서 살펴본 내용들은 curl을 이용해 직접적으로 이더리움 클라이언트에게 RPC 요청을 보내는 방법이었다. 하지만 요청을 보낼 때마다 이러한 형식을 지켜서 보내는 것엔 상당한 불편함이 따른다. 이더리움 재단에서는 Web3.js 라는 JavaScript 라이브러리를 제공하고 있는데 해당 라이브러리를 통해 이더리움 네트워크와 상호작용 할 수 있는 다양한 메소드를 제공받을 수 있다. 한마디로 이더리움 클라이언트에 RPC 요청을 쉽게 보낼 수 있게 해주는 라이브러리 라고 생각하면 된다.
Jest를 사용해 테스트 코드를 작성해보는 형식으로 Web3 라이브러리에서 제공해주는 메소드들의 사용법을 익혀보도록 하자. 우선 web3 와 Jest를 설치해준 다음 jest.config.js 파일 안에 다음과 같이 작성해주었다.
$ npm init -y
$ npm install -D jest
$ npm install web3
// jest.config.js 파일
const config = {
verbose: true,
testMatch: ['<rootDir>/**/*.test.js'],
};
module.exports = config;
web3.test.js 파일을 하나 생성하여 web3 관련 메소드들의 테스트 코들를 작성해보자.
👉 web3 연결 테스트
현재 가나쉬를 통해 실행되고 있는 이더리움 클라이언트는 http에서 동작하는 노드라고 볼 수 있다. 따라서 HttpProvider( ) 메소드를 사용해 web3 인스턴스를 생성해주었다.
// web3 테스트 코드
const Web3 = require('web3');
describe('web3 테스트 코드', () => {
let web3;
it('web3 연결 테스트', () => {
// new Web3.providers.HttpProvider("http://127.0.0.1:8545");
web3 = new Web3(new Web3.providers.HttpProvider('http://127.0.0.1:8545'));
});
});
👉 최신 블록 높이(number) 가져오기
web3.eth.getBlockNumber( ) 메소드를 사용해 최신 블록의 높이를 가져올 수 있다. 이더리움 네트워크에서는 height이 아닌 number로 블록의 높이를 나타낸다.
// web3 테스트 코드
const Web3 = require('web3');
describe('web3 테스트 코드', () => {
let web3;
it('web3 연결 테스트', () => {
// new Web3.providers.HttpProvider("http://127.0.0.1:8545");
web3 = new Web3(new Web3.providers.HttpProvider('http://127.0.0.1:8545'));
});
// 최신 블록 높이(number) 가져오기
it('Latest Block 높이(number) 가져오기', async () => {
const latestBlock = await web3.eth.getBlockNumber();
console.log(latestBlock);
});
});
👉 전체 accounts 가져오기
web3.eth.getAccounts( ) 메소드를 사용해 해당 노드에 존재하는 계정들을 가져올 수 있다. 이후 트랜잭션을 발생시키는 테스트 코드를 작성하기 위해 가져온 계정들에서 sender 변수에 첫번째 계정을 할당하고 received 변수에 두번째 계정을 할당했다.
// web3 테스트 코드
const Web3 = require('web3');
describe('web3 테스트 코드', () => {
let web3;
let accounts;
let sender; // 보내는 사람
let received; // 받는 사람
it('web3 연결 테스트', () => {
// new Web3.providers.HttpProvider("http://127.0.0.1:8545");
web3 = new Web3(new Web3.providers.HttpProvider('http://127.0.0.1:8545'));
});
it('전체 accounts 가져오기', async () => {
// 현재 가나쉬에 있는 accounts
accounts = await web3.eth.getAccounts();
sender = accounts[0];
received = accounts[1];
console.log(accounts);
});
});
👉 balance 가져오기
web3.eth.getBalance( ) 메소드를 사용해 특정 계정의 잔액을 조회할 수 있다. 인자값으로 계정을 전달해주기만 하면 된다. 참고로 이더리움 네트워크 상에서 balance의 기본 단위는 wei이다. 가장 많이 사용되는 단위는 wei , Gwei , Ether 이며 아래의 그림은 이더리움 단위 환산표이다.
- wei : 1
- 1 Gwei = 10 ** 9 wei
- 1 Ether = 10 ** 18 wei
// web3 테스트 코드
const Web3 = require('web3');
describe('web3 테스트 코드', () => {
let web3;
let accounts;
let sender; // 보내는 사람
let received; // 받는 사람
it('web3 연결 테스트', () => {
// new Web3.providers.HttpProvider("http://127.0.0.1:8545");
web3 = new Web3(new Web3.providers.HttpProvider('http://127.0.0.1:8545'));
});
it('전체 accounts 가져오기', async () => {
// 현재 가나쉬에 있는 accounts
accounts = await web3.eth.getAccounts();
sender = accounts[0];
received = accounts[1];
console.log(accounts);
});
it('첫번째 계정 balance 가져오기', async () => {
// getBalance() 메소드에는 인자값 존재
const balance = await web3.eth.getBalance(accounts[0]);
console.log(balance); // wei 단위로 Ether를 표현함.
console.log('ETH : ', balance / 10 ** 18);
/**
* 이더리움의 단위
* wei : 1
* Gwei : 10 ** 9 wei
* Ether : 10 ** 18 wei
*
* */
});
});
👉 ETH 단위 변환하기
web3.utils.toWei( ) 메소드를 사용해 이더리움 balance 값들의 단위를 변환할 수 있다. toWei( '1' , 'gwei' ) 메소드를 사용해 1 gwei를 wei 단위로 변환했으며 toWei( '1' , 'ether' ) 메소드를 사용해 1 ether를 wei 단위로 변환해주었다.
// web3 테스트 코드
const Web3 = require('web3');
describe('web3 테스트 코드', () => {
let web3;
let accounts;
let sender; // 보내는 사람
let received; // 받는 사람
it('web3 연결 테스트', () => {
// new Web3.providers.HttpProvider("http://127.0.0.1:8545");
web3 = new Web3(new Web3.providers.HttpProvider('http://127.0.0.1:8545'));
});
it('ETH 단위 변경하기', () => {
console.log(web3.utils.toWei('1', 'gwei')); // 1 Gwei를 wei 단위로 변환
console.log(web3.utils.toWei('1', 'ether')); // 1 Ether를 wei 단위로 변환
});
});
👉 트랜잭션 횟수 조회하기
web3.eth.getTransactionCount( ) 메소드를 사용해 인자값으로 전달한 계정이 발생시킨 트랜잭션의 횟수를 조회할 수 있다. 그리고 web3.utils.toHex( ) 메소드를 사용해 트랜잭션 횟수를 hex 단위로 변환해주었다.
// web3 테스트 코드
const Web3 = require('web3');
describe('web3 테스트 코드', () => {
let web3;
let accounts;
let sender; // 보내는 사람
let received; // 받는 사람
it('web3 연결 테스트', () => {
// new Web3.providers.HttpProvider("http://127.0.0.1:8545");
web3 = new Web3(new Web3.providers.HttpProvider('http://127.0.0.1:8545'));
});
it('전체 accounts 가져오기', async () => {
// 현재 가나쉬에 있는 accounts
accounts = await web3.eth.getAccounts();
sender = accounts[0];
received = accounts[1];
console.log(accounts);
});
it('트랜잭션 횟수 구해오기', async () => {
const txCount = await web3.eth.getTransactionCount(sender);
console.log(txCount);
// 트랜잭션 횟수를 hex 단위로 변환
console.log(web3.utils.toHex(txCount));
});
});
👉 트랜잭션 실행하기
트랜잭션을 발생시키기 위한 테스트 코드를 작성해 보고자 한다. 우선 트랜잭션 객체(txObject) 안에 들어가게 되는 속성값들은 다음과 같다.
- nonce : 발신자 계정이 발생시킨 총 트랜잭션 횟수
- from : 발신자 계정
- to : 수신자 계정
- value : 보낼 금액 ( 단위 : wei )
- gasLimit : 해당 트랜잭션이 사용할 수 있는 가스의 최대치 ( 단위 : gas )
- gasPrice : 발신자가 지불하고자 하는 가스 당 가격 ( 단위 : Gwei / gas )
- data : 스마트 컨트랙트와 관련된 데이터
여기서 한가지 짚고 넘어가야할 것은 바로 가스(gas)에 관한 내용이다. 가스란 쉽게 말해 이더리움 네트워크에서 트랜잭션을 실행하는데 필요한 연료이다. 즉, EVM 상에서 트랜잭션을 실행시키기 위해 소모되는 비용을 의미하며 트랜잭션을 생성하는 쪽에서 해당 트랜잭션을 실행시키기 위해 지불하는 수수료와 같은 개념이다. 그리고 이러한 트랜잭션 실행 비용을 가스비(gas fee)라고 부르며 가스비는 gwei 단위로 가격이 측정된다.
gasLimit(가스 한도)은 트랜잭션을 발생시키는 쪽에서 해당 트랜잭션에 대해 사용하고자 하는 최대 가스량을 의미하며 gasPrice(가스 가격)는 지불하고자 하는 가스 당 가격이다. 트랜잭션에서 과도한 연산이 발생해 소모되는 gas의 양이 gasLimit 을 넘어설 경우 해당 트랜잭션에 대한 처리는 중단된다. gasLimit * gasPrice는 지불 가능한 최대 수수료를 의미하며 트랜잭션이 실행될 때 트랜잭션을 생성한 계정에서 해당 금액만큼이 차감된다. 만약 트랜잭션을 실행시키는데 소모된 gas의 양(gasUsed)이 gasLimit 보다 낮다면 차액( (gasLimit - gasUsed) * gasPrice )만큼 환불된다.
추가로 트랜잭션 객체 안에 들어가는 nonce 속성값은 블록(Block)의 속성값으로 들어가는 nonce와는 별개의 의미를 갖는다. 블록 속성으로서의 nonce가 PoW에 사용되는 값이었다면 , 트랜잭션 객체 안의 nonce 값은 발신자 계정이 발생시킨 트랜잭션 횟수를 의미한다. 계정의 상태를 체크하기 위해 nonce라는 일렬번호를 넣어준 것이라고 생각하면 된다.
트랜잭션 객체(txObject)를 만들었다면 ethereumjs-tx 라이브러리를 사용해 트랜잭션 객체를 이더리움 클라이언트가 이해할 수 있도록 만들어주면 된다.
$ npm install ethereumjs-tx
const ethTx = require('ethereumjs-tx').Transaction
const tx = new ethTx(txObject)
tx.sign(privateKey) // tx 객체 안에 서명(signature)값 추가하기
const serializedTx = tx.serialize()
const TxObject = await web3.eth.sendSignedTransaction('0x' + serializedTx.toString('hex'));
new ethTx( ) 의 인자값으로 txObject를 전달해 이더리움 클라이언트가 이해할 수 있는 tx 객체를 만들어 주었으며 tx.sign( ) 메소드를 사용해 tx 객체 안에 서명(signature)의 내용을 추가해준다. 이 때 tx.sign( ) 의 인자값으로는 비밀키(privateKey)를 전달한다.
마지막으로 tx.serialize( ) 메소드를 사용해 tx를 정렬해준 다음 web3.eth.sendSignedTransaction( ) 메소드를 이용해 트랜잭션 내용을 전송해주면 된다.
// web3 테스트 코드
const Web3 = require('web3');
const ethTx = require('ethereumjs-tx').Transaction;
describe('web3 테스트 코드', () => {
let web3;
let accounts;
let sender; // 보내는 사람
let received; // 받는 사람
it('web3 연결 테스트', () => {
// new Web3.providers.HttpProvider("http://127.0.0.1:8545");
web3 = new Web3(new Web3.providers.HttpProvider('http://127.0.0.1:8545'));
});
it('전체 accounts 가져오기', async () => {
// 현재 가나쉬에 있는 accounts
accounts = await web3.eth.getAccounts();
sender = accounts[0];
received = accounts[1];
console.log(accounts);
});
it('트랜잭션 실행하기', async () => {
// 0xca4d95b225d1a7a48b994da481c7d7fb3eedf0a33f84f89b28868499bcc794da (보내는 사람 개인키)
// 앞의 0x 제거
const privateKey = Buffer.from('ca4d95b225d1a7a48b994da481c7d7fb3eedf0a33f84f89b28868499bcc794da', 'hex');
const txCount = await web3.eth.getTransactionCount(sender);
const txObject = {
nonce: web3.utils.toHex(txCount), // 보내는 사람이 발생시킨 트랜잭션 횟수
from: sender,
to: received,
value: web3.utils.toHex(web3.utils.toWei('1', 'ether')), // 보낼 금액 (단위를 wei로 해야한다. 10 ** 18 -> hex)
gasLimit: web3.utils.toHex(6721975), // 해당 트랜잭션이 사용할 수 있는 가스의 최대치
gasPrice: web3.utils.toHex(web3.utils.toWei('1', 'gwei')), // 발신자가 지불하는 가스 당 가격
data: web3.utils.toHex(''), // 스마트 컨트랙트와 관련된 data
};
const tx = new ethTx(txObject);
tx.sign(privateKey); // tx.sign() 메소드를 사용하면 tx 객체 안에 서명 값을 추가해준다.
console.log(tx);
const serializedTx = tx.serialize();
console.log(serializedTx.toString('hex'));
const TxObject = await web3.eth.sendSignedTransaction('0x' + serializedTx.toString('hex'));
console.log(TxObject);
});
});
3. web3 기본 메소드 정리
- web3.eth.getBlockNumber( ) : 최신 블록의 높이(number) 가져오기
- web3.eth.getAccounts( ) : 계정 가져오기
- web3.eth.getBalance( account ) : 계정 잔액 조회하기
- web3.utils.toWei( '1' , 'ether' ) : 1 ether를 wei 단위로 변환하기
- web3.utils.toHex( ) : hex 단위로 변환하기
- web3.eth.getTransactionCount( account ) : 계정이 발생시킨 트랜잭션 횟수 조회하기
- web3.eth.sendSignedTransaction( ) : 트랜잭션 전송하기
'Ethereum' 카테고리의 다른 글
Ethereum/이더리움 - private 네트워크 RPC 설정하기 (1) | 2022.07.01 |
---|---|
Ethereum/이더리움 - private 네트워크 (1) | 2022.06.30 |
Ethereum/이더리움 - 메타마스크 연결하기 (5) | 2022.06.29 |
Ethereum/이더리움 - 비트코인 vs 이더리움 (0) | 2022.06.28 |
Ethereum/이더리움 - 개발 환경 세팅 (Go , Geth , Ganache) (0) | 2022.06.28 |