Infura가 제공하는 provider를 이용하여 이더리움 클라이언트에 원격으로 블록체인 온체인 데이터를 요청해봤다. 이는 web3 라이브러리를 사용하면 쉽게 가능하다. 하지만 Infura에서는 sendTransaction이라는 메소드를 지원하지 않는다. 덕분에 http Provider를 이용할 때는 블록체인 상에 transaction을 보내기 위해서는 어렵게 어렵게 보내야 한다.
이번에는 rawTransaction으로 배포되어있는 ERC-20 Contract의 transfer 함수를 실행시키는 것을 web3로 구현해보려고 한다.
provider란게 결국 이더리움 네트워크에 연결된 이더리움 클라이언트에 명령을 보낼 수 있게 하는 것이다. 결국 geth나 parity로 작동하고 있는 이더리움 클라이언트가 있을것이고, 우리는 해당 클라이언트에 web3를 이용해서 원격으로 원하는 데이터를 요청하는 것이다.
본래 Transaction을 보내려면 transaction 메타데이터를 개인키로 서명을 해야한다. 하지만 원격으로 http 요청만 받고 있는 이더리움 클라이언트가 현재 내 컴퓨터(위험하지만)나 머릿 속에 있는(거의 불가능 하지만) 개인키를 알 턱이 있을까.
사실 web3.eth.accounts.wallet
을 이용하면 개인키를 이용해서 계정을 만들고 등록을 할 수 있긴하다. 하지만 개인키를 직접 http 요청과 함께 해당 클라이언트에게 보내는 것은 상당히 위험한 일이다. 도중에 개인키가 가로채질 수도 있고, 전송하는 클라이언트가 공용으로 사용되어지고 있으니 등록되면 노출될 위험이 있다.
하지만 내 단말기에서 개인키로 transaction을 서명해서 서명된 데이터만 보낸다면 비교적 안전한 방법이라 할 수 있겠다. rawTransaction은 이에대한 일환이다.
express 서버와 web3 라이브러리를 이용하여 구현한다.
[web3 객체와 contract 객체 준비]
const web3 = new Web3(process.env.PROVIDER_URI);
const RockCoin = web3.Contract(contractABI, contractAddr);
web3를 Infura에서 받은 Provider URI로 초기화해준다. RockCoin은 미리 만든 ERC-20 토큰으로 스마트 컨트랙트를 컴파일하고 Goerili에 배포한 이후에 얻은 ABI와 컨트랙트 주소를 인자로 전해주어 초기화해줬다.
[컨트랙트 함수 요청 데이터 생성]
const data = RockCoin.methods.transfer(req.body.recipient,req.body.amount).encodeABI();
EVM은 결국 bytescode를 해석해서 스마트컨트랙트를 함수를 호출하게 된다. bytescode는 구조가 처음 8바이트가 함수명, 이후 데이터는 해당 함수가 호출되기 위한 인자 값들이 각각 32 bytes 로 이어 붙는다. 형태는 16진수로 나타나게 된다.
위에 작성된 코드는 web3 라이브러리가 해당 과정을 단 하나의 함수 encodeABI
로 결과값까지 받아낼 수 있음을 보여준다.
[Transaction 데이터 생성]
const tx = {
from: process.env.CURRENT_ACCOUNT,
to: "0xe0C753D9d5Bad3b93BACCBeD5821b6d439B49f22",
gas: 50000,
data
}
다른 transaction 데이터들도 포함할 수 있지만, 이번에 필요한 제일 간단한 transaction을 생성해주었다. to
에는 실행시키고자 하는 컨트랙트 주소를 기입하면 되고, 이번 구현같은 경우는 미리 배포되어 있는 ERC-20 토큰 컨트랙트 주소를 기입했다. data
에는 방금 설명했던 함수와 인자값 데이터들이 들어온다.
[Transaction 서명]
const txSigned = await web3.eth.accounts.signTransaction(tx, process.env.CURRENT_PRIVATEKEY)
.catch(err=>{
console.log(err);
return res.status(500).send("Internal Error");
});
web3.eth.accounts.signTransaction
함수가 핵심이다. 인자로는 아까 생성해준 transaction 데이터와 transaction 전송 주체의 개인키를 넘겨준다. 결과값으로 여러 값들이 있는 객체가 주어지는데 그 중 rawTransaction 데이터가 이번에 목표로 도출하고자 했던 값이다.
생각해보니까 결국 provider에 개인키를 전송하는 것은 똑같다. 도중에 개인키가 노출되는 문제는 여전한 것 같다.
[Provider에 JSON-RPC로 rawTransaction 요청하기]
return await axios.post(process.env.PROVIDER, {
jsonrpc: "2.0",
method: "eth_sendRawTransaction",
params: [txSigned.rawTransaction],
id: 1
})
.then(result=>{
console.log(result.data);
return res.status(202).send(result.data);
})
.catch(err=>{
console.log(err);
return res.status(404).send(err);
})
여기서는 JSON-RPC 형식이 핵심이다. 사실 지금껏 web3.eth.*
형식으로 사용하던 것이 모두 이런 식으로 치환 가능하다는 것을 알 수 있다. method eth_sendRawTransaction
은 web3.eth.sendRawTransaction
가 될 수 있다. 하지만 sendRawTransaction 같은 경우는 web3에서는 지원해주지 않는 듯 하다. params에는 해당 함수 호출에 필요한 인자를 보내주면 된다. 이번 같은 경우는 rawTransaction 데이터이다.
결과값으로는 똑같이 JSON-RPC
형식으로 생성된 transaction 해쉬값이 포함되어 돌아온다.
[전체코드]
const web3 = new Web3(process.env.PROVIDER_URI);
const RockCoin = web3.Contract(contractABI, contractAddr);
const sendToken = async (req, res) =>{
if(!req.body.recipient || !req.body.amount)
return res.status(400).send("Require All Parameter for Transfer Token");
const data = RockCoin.methods.transfer(req.body.recipient,req.body.amount).encodeABI();
const tx = {
from: process.env.CURRENT_ACCOUNT,
to: "0xe0C753D9d5Bad3b93BACCBeD5821b6d439B49f22",
gas: 50000,
data
}
const txSigned = await web3.eth.accounts.signTransaction(tx, process.env.CURRENT_PRIVATEKEY)
.catch(err=>{
console.log(err);
return res.status(500).send("Internal Error");
});
return await axios.post(process.env.PROVIDER, {
jsonrpc: "2.0",
method: "eth_sendRawTransaction",
params: [txSigned.rawTransaction],
id: 1
})
.then(result=>{
console.log(result.data);
return res.status(202).send(result.data);
})
.catch(err=>{
console.log(err);
return res.status(404).send(err);
})
}
생각보다 많은 기능을 지원해주는 web3 라이브러리다. 이 때문에 직접 클라이언트가 되지 않더라도 충분히 클라이언트들의 기능을 구현하여 사용할 수 있다.
또한 해커들만 사용할 것 같은 bytescode 도 이렇게 함수 하나로 구현될 수 있기 때문에 무궁무진하게 사용할 수 있을 것 같다.