대부분의 설정은 기존의 타입스크립트 환경과 유사함
npm init -y
// 타입스크립트 관련
npm i -D typescript ts-node @types/node
// lint 관련
npm i -D eslint prettier eslint-plugin-prettier eslint-config-prettier
// jest 관련
npm i -D ts-jest @types/jest babel-core @babel/preset-typescript @babel/preset-env
// web3와 ether.js
npm i web3 ethereumjs-tx @types/web3
❗️설정하면서 중간에 이것저것 받으면서 web3-typescript-typings 이라는 라이브러리를 받았었는데 얘를 받으니까 설정된 타입이 web3라이브러리랑 달라서 함수의 인자나 리턴값 등이 맞지 않는 이슈가 있었다. 버전문제인지는 모르겠지만 일단 이 라이브러리는 사용하지 않는걸로..
{
"compilerOptions": {
"outDir": "./dist/",
"esModuleInterop": true,
"moduleResolution": "node",
"resolveJsonModule": true,
"strict": true,
"baseUrl": ".",
"typeRoots": ["./node_modules/@types", "./@types"],
"paths": {
"@core/*": ["src/core/*"],
"*": ["@types/*"]
}
}
}
module.exports = {
presets: [
[
'@babel/preset-env',
{
targets: { node: 'current' },
},
],
'@babel/preset-typescript',
],
};
import type { Config } from '@jest/types';
const config: Config.InitialOptions = {
moduleFileExtensions: ['js', 'ts', 'json', 'jsx', 'tsx', 'node'],
testMatch: ['<rootDir>/**/*.test.(js|ts)'],
testEnvironment: 'node',
verbose: true,
preset: 'ts-jest',
};
export default config;
.eslintrc
{
"extends": ["plugin: prettier/recommended"]
}
.prettierrc
{
"printWidth": 120,
"tabWidth": 2,
"singleQuote": true,
"traillingComma": "all",
"semi": true
}
web3.js와 ethers.js
Web3.js와 ethers.js는 모두 개발자가 Ethereum 블록 체인과 상호 작용할 수 있도록하는 기능을하는 JavaScript 라이브러리이다. 경우에 따라 둘 중 더 적합한 라이브러리가 있어 본 게시글에서는 두 라이브러리를 병행해서 사용하고 있다.
Web3에 앞서 먼저 rpc에 대해 알아보자.
web3에 대한 설명을 찾아보면
json-rpc 프로토콜로 이더리움 노드와 통신하는 기능을 하는 라이브러리
라는 설명을 볼 수 가 있는데 rpc를 알아야 좀 더 쉽게 이해가 되기 때문이다.
rpc는 REST와 같은 API 아키텍쳐의 일종이다. http통신을 할 때 지금까지는 REST방식으로 GET/POST/PUT/DELETE 등의 메소드를 사용해왔다.
RPC방식은 원격 프로시저 호출이라는 의미로 다른 컨텍스트에서 함수의 원격 실행을 허용하는 사양이다.
클라이언트에서 직접적으로 함수단위로 호출해서 사용이 가능한 이 방식을 이더리움 네트워크가 채택하고 있기 때문에 우리는 rpc방식으로 통신을 하여야한다.
이를 curl 형태로 나타내면 다음과 같다.
curl -X POST \
-H "Content-type: application/json" \
--data '{"jsonrpc" : "2.0", "method": "eth_accounts", "params": []}' \
http://localhost:8545
가나쉬를 실행한 뒤 위 코드를 통해 가나쉬에 요청을 보내면 응답을 주는데,
data에 jsonrpc의 버전정보
와 method
에 함수명을 입력하면 서버측에서 해당 함수가 실행되면서 함수의 리턴값을 응답으로 보내주는 형태의 통신이 일어난다.
아래는 위 요청에 대한 가나쉬 서버의 응답이다.
{
"jsonrpc":"2.0",
"result":[
"0x0c4990165af30f3259b217082ce4ab90dfc04754",
"0x52147030561859690cca20d92cdcd430412e5966",
"0x6c6b900f8e7817cfb0178bea913477d9bcaf0720",
"0x2da56cc87da6124e0392894ff1446fec69356f20",
"0xd0e28f8a7692ff765c21c1899ca510b95f7821e0",
"0x1b12fb9b61ac1690d66a176a2eab74f250161b84",
"0xf63a9b0dbd8173d857a4f79fa6b9e4e5d8045abe",
"0x7b51470ae94f08cdf844df4a998393e61efa909f",
"0x726ad88ef8321e13c604f8615b6af3b26f7bb6bd",
"0x6dceb89b7351c9a6686a99946882851e75dacd0b"
]
}
jsonrpc를 명시해주고 실제 data는 result에 담겨있다.
위의 약간은 귀찮은 data 전송을 메소드 하나로 쉽게 할 수 있도록 해준다. 내부적으로 데이터형식의 변환이 필요한 곳도 알아서 처리를 해주기 때문에 사용자는 어떤 함수를 실행할지, 함수의 인자값으로 보낼 params에는 뭘 넣을지 정도만 작성해주면 된다.
web3 라이브러리를 이용하면 브라우저든 노드든 상관없이 어디서든 블록체인네트워크와의 쉬운 통신이 가능하다!
코드는 아래와 같다.
const web3 = new Web3(new Web3.providers.HttpProvider('http://127.0.0.1:8545'));
const accounts = await web3.eth.getAccounts();
console.log(accounts)
/* 콘솔
[
"0x0c4990165af30f3259b217082ce4ab90dfc04754",
"0x52147030561859690cca20d92cdcd430412e5966",
"0x6c6b900f8e7817cfb0178bea913477d9bcaf0720",
"0x2da56cc87da6124e0392894ff1446fec69356f20",
"0xd0e28f8a7692ff765c21c1899ca510b95f7821e0",
"0x1b12fb9b61ac1690d66a176a2eab74f250161b84",
"0xf63a9b0dbd8173d857a4f79fa6b9e4e5d8045abe",
"0x7b51470ae94f08cdf844df4a998393e61efa909f",
"0x726ad88ef8321e13c604f8615b6af3b26f7bb6bd",
"0x6dceb89b7351c9a6686a99946882851e75dacd0b"
]
*/
web3 라이브러리로 실행 가능한 함수들은 여기서 확인할 수 있다.
이더리움 관련 메소드로는
등이 있고 그 외에도
와 같은 단위변환 util 함수도 있다.
기본적으로 web3와 비슷한 기능을 한다고 보면 될 것 같다. 이더리움 블록체인 생태계와의 상호작용을 하기위한 라이브러리로 정보를 받아오고 트랜잭션을 발생시킬 수도 있다.
web3에 비해서는 자료가 많이 없다...
오늘 공부한 파트에서는 이 라이브러리를 트랜잭션 객체를 생성하는 용도로 사용했다.
import * as ether from 'ethereumjs-tx';
const ethTx = ether.Transaction;
// 👇 트랜잭션 객체의 내용은 하단에서 더 자세히 설명할 예정
const txObject: ether.TxData = {
nonce: web3.utils.toHex(txCount),
to: receiver,
value: web3.utils.toHex(web3.utils.toWei('1', 'ether')),
gasLimit: web3.utils.toHex(6721975),
gasPrice: web3.utils.toHex(web3.utils.toWei('1', 'gwei')),
data: web3.utils.toHex(''),
};
const tx = new ethTx(txObject)
지난 몇 주간 직접 블록체인 서버를 구축하고 비트코인 방식의 트랜잭션을 코드로 구현해보았다.
이더리움의 트랜잭션은 비트코인의 트랜잭션과 다음과 같은 차이가 있다
nonce
가 있어서 한 account에서 발생된 트랜잭션간의 순서로 balance를 파악한다.새로 계정을 생성하게 되면 nonce는 0
첫번째 트랜잭션을 전송하게 되면 nonce가 1
두번째 트랜잭션을 전송하게 되면 nonce가 2
이렇게 전송할 때마다 증가하는 식이다.
만약 이 상태에서 nonce가 4인 트랜잭션을 처리하려고 한다면 nonce가 3인 트랜잭션의 전송내역이 있어야 한다. 처리되지 않은 nonce 3인 트랜잭션은 풀에 남아있다가 3,4가 연달아 처리되게 된다.
nonce는 중복되지 않고 순차적이기 때문에, 같은 nonce 에 여러 트랜잭션 전송이 발생하였다면 해당 nonce 중 제일 높은 가스비를 지불한 트랜잭션이 처리된다. 이더리움에서는 이러한 방법으로 이중 지불 문제를 방지
할 수 있다.
너무 낮은 가스비를 지불한 경우에는 (아직 트랜잭션이 pending상태인 경우) 같은 nonce로 가스비를 높여 다시 트랜잭션을 보내게 된다면 취소된 효과를 볼 수도 있다.
const txObject: ether.TxData = {
nonce: web3.utils.toHex(txCount), // 보내는 사람의 트랜잭션 수 (현재 nonce를 보내면 알아서 +1해줌)
to: receiver, // 받는 사람의 account
value: web3.utils.toHex(web3.utils.toWei('1', 'ether')), // 전송할 금액. 단위로 wei를 써야함 + hex 변환
gasLimit: web3.utils.toHex(6721975), // 트랜잭션의 최대 가스량
gasPrice: web3.utils.toHex(web3.utils.toWei('1', 'gwei')), // 가스 단위 가격, wei
data: web3.utils.toHex(''), // 지금은 작성 X. 스마트컨트랙트가 트랜잭션에 포함될 때 해당 내용을 넣음
};
위에서 언급했듯 비트코인의 트랜잭션과는 내용이 조금 다른데,
utxo
관련 속성이 빠지고 nonce
와 gas
관련된 내용이 추가되었다.
이더리움은 evm을 통한 트랜잭션 및 컨트랙트 처리가 이루어지므로 해당 자원을 이용하는 수수료를 지불하게 되는데 이 비용이 바로 gas
이다.
기본적으로 트랜잭션 전송 시에는 21000의 gas를 소비하게 되고
추가적인 연산의 양에 따라 추가적인 gas를 소모하게 된다.
다른 코드가 없는 트랜잭션에는 balance의 추가/감소 연산만 있어 4gas를 소모하며 결과적으로 21004라는 gas가 책정되게 된다.
etherjs 라이브러리를 사용하여 트랜잭션 인스턴스를 생성한 뒤 서명을 한다.
sign메소드와 serialize메소드를 이용해 서명
등을 생성한다. 이 때 서명에 사용된 private key로 from
account 정보가 자동으로 생성되어 최종 트랜잭션 객체에 포함된다.
web3.eth.sendSignedTransaction() 메소드를 사용하여 만들어진 트랜잭션 인스턴스를 블록체인 서버 (지금은 가나쉬)로 보내고 해당 트랜잭션이 블록에 올라가게된다.
const tx = new ethTx(txObject); // tx 객체 생성
tx.sign(privateKey); // tx 객체에 서명 추가 (return값이 void)
const serializedTx = tx.serialize();
const txHash = await web3.eth.sendSignedTransaction('0x' + serializedTx.toString('hex'));
{
transactionHash: '0x65b95bc07d1e8847b8eeb32c6113cc0dbf49b9e8d21c0b4704032758ca06ddf3',
transactionIndex: 0,
blockHash: '0x71eb9d6914284d9cce0316dcbccb8be5239484edeea1def5d2edec0c75f2f4e9',
blockNumber: 5,
from: '0x0c4990165af30f3259b217082ce4ab90dfc04754',
to: '0x52147030561859690cca20d92cdcd430412e5966',
gasUsed: 21004,
cumulativeGasUsed: 21004,
contractAddress: null,
logs: [],
status: true,
logsBloom: '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'
}
💬 테스트 파일의 전체 내용은 github 참조