loom network의 blockchain explorer를 만들어 보자.
한동안 guns.db와 함께 관심갖고 보았던 loom에서 event를 하고 있는데 재미있어보여 블록 탐색기부터 시도해보기로 한다.
https://loomx.io/developers/en/block-explorer-tutorial.html#overview
튜토리얼이 있긴 한데 말이 튜토리얼이지 github 소스하나 툭 던지고 뭐 없다.
성격도 급하고 일단 빨랑 제출해야 되니까 github 소스 받아서 실행하고 chrome 의 network tab 을 열었다.
Request URL: https://plasma.dappchains.com/rpc/blockchain?minHeight=12668090&maxHeight=12668099
요런거랑
Request URL: https://plasma.dappchains.com/rpc/status
요런거 두개 가 보였다. 더 뭔가 없을까 해서 이것저것 처보다가.
https://plasma.dappchains.com/rpc 여기를 보니
사용가능한 endpoints 들이 쭉 나온다.
Available endpoints:
//plasma.dappchains.com/abci_info
//plasma.dappchains.com/consensus_state
//plasma.dappchains.com/dump_consensus_state
//plasma.dappchains.com/genesis
//plasma.dappchains.com/health
//plasma.dappchains.com/net_info
//plasma.dappchains.com/num_unconfirmed_txs
//plasma.dappchains.com/status
Endpoints that require arguments:
//plasma.dappchains.com/abci_query?path=_&data=_&height=_&prove=_
//plasma.dappchains.com/block?height=_
//plasma.dappchains.com/block_results?height=_
//plasma.dappchains.com/blockchain?minHeight=_&maxHeight=_
//plasma.dappchains.com/broadcast_tx_async?tx=_
//plasma.dappchains.com/broadcast_tx_commit?tx=_
//plasma.dappchains.com/broadcast_tx_sync?tx=_
//plasma.dappchains.com/commit?height=_
//plasma.dappchains.com/consensus_params?height=_
//plasma.dappchains.com/mempool_txs?limit=_
//plasma.dappchains.com/nonce?key=_&account=_
//plasma.dappchains.com/subscribe?query=_
//plasma.dappchains.com/tx?hash=_&prove=_
//plasma.dappchains.com/tx_search?query=_&prove=_&page=_&per_page=_
//plasma.dappchains.com/unconfirmed_txs?limit=_
//plasma.dappchains.com/unsubscribe?query=_
//plasma.dappchains.com/unsubscribe_all?
//plasma.dappchains.com/validators?height=_
뭐 이정도면 충분하지 않을까 싶어 바로 착수.
codepen.io에 대충 반들어보니까
https://codepen.io/acidsound/pen/qBWGxpR
생각보다 어렵지 않게 되었다.
필요한건 뭐? 스피드!
언제나 좋아하는 황금조합인
parcel+github+netlify 로 외부 노출 주소, https 설정, CI까지 한번에 3분만에 완료.
package.json을 아래와 같이 만들고 코딩을 시작해본다.
{
"name": "loomxplorer",
"version": "1.0.0",
"main": "index.html",
"scripts": {
"build": "parcel build index.pug"
},
"license": "MIT",
"devDependencies": {
"cssnano": "^4.1.10",
"parcel": "^1.12.3",
"pug": "^2.0.4",
"stylus": "^0.54.7"
},
"dependencies": {
"date-fns": "^2.4.1"
}
}
scripts 에 build 를 넣는게 포인트. build script 를 실행하면 dist 디렉토리에 떨어뜨려준다.
netlify에서 build & deploy > Continuous Deployment에 아래와 같이 되도록 설정한다. npm run build 와 /dist정도만 잘 쓰면 문제 없다.
Build settings
Repository: github.com/acidsound/loomXplorer
Base directory: Not set
Build command: npm run build
Publish directory: /dist
Deploy log visibility: Logs are public
RPC API 중에 쓸만한 걸 꼽아보니.
//plasma.dappchains.com/rpc/status (상태)
//plasma.dappchains.com/rpc/block?height=_ (특정블록의 정보)
//plasma.dappchains.com/rpc/block_results?height=_ (블록결과)
//plasma.dappchains.com/rpc/blockchain?minHeight=_&maxHeight=_ (블록하이트 범위 지정 쿼리)
정도?
일단 js 부터.
import 해줄게 뭐가 있지?
import 'regenerator-runtime/runtime'
import { formatDistance } from 'date-fns'
일단 async/await을 쓸테니까 'regenerator-runtime/runtime'하고 시간도 보여줄꺼니까 무거운 moment대신 date-fns를 써서 formatDistance를 가져다 놓자.
API들 먼저 정의해놓자.
const cmds = {
"endpoint": "https://plasma.dappchains.com/rpc",
"status": "/status",
"blockheight": "/blockchain?",
"block": "/block?height=",
"tx": "/tx?hash=",
}
요정도면 충분.
fetch를 조금 개조해서 json에 result만 가져오는 걸 하나 만들자.
const afetch = async (url, options={})=>
(await (await fetch(url, options)).json()).result
닭질이 많이 줄어든다.
이걸로 현재 status랑 min~maxHeight 까지 목록 조회하는 걸 만들어본다. status는 마지막 blockHeight를 가져오기 위한 용도이면 min은 이 blockHeight-9로 설정하려고 한다.
const getChainStatus = async ()=>
await afetch(`${cmds.endpoint}${cmds.status}`)
const getBlocks = async ({from, to})=>
await afetch(`${cmds.endpoint}${cmds.blockheight}minHeight=${from}&maxHeight=${to}`)
아주 아주 간단하다.
보니까 시간이 ISOTime 형식으로 나오는데 현재 시간으로부터 얼마나 지났는지 알려주는 함수도 하나 만들자.
const getSinceFrom = since => formatDistance(Date.parse(since), new Date())
react나 vue같은 걸 써서 이쁘게 해도 되겠지만 시간이 없으니까 빠르게 DOM을 생성하는 걸 만들어서 집어넣자.
ul#blocks__meta
li.row.hash.obj
.head
.desc
.transaction
span
span
| transactions
.validator
span.highlightedText
| Validator
span.link
.since
목록이 되는 리스트인데 pug가 익숙하지 않은 분들을 위해 html로 쓰면
<ul id="blocks__meta">
<li class="row hash obj">
<div class="head"> </div>
<div class="desc">
<div class="transaction"><span></span><span>transactions</span></div>
<div class="validator"><span class="highlightedText"> Validator</span><span class="link"></span></div>
<div class="since"></div>
</div>
</li>
</ul>
이렇게 작성했다.
html보단 pug를, css보단 stylus를, js보단 coffeescript(이번엔 js로 했다)를 코드 양이 적어서 선호하는 편인데 .obj 라는 클래스를 일단 만들어넣고 얘는 display: none 으로 안보이게 한 뒤 deep copy해서 쓰는 식으로 목록을 생성하게 했다.
일정 주기마다 blockheight를 받아오는 API를 호출하고 받아온 값을 가지고 렌더링하는 함수를 만든다.
const updateHashLists = ({blockMetas})=> {
const list = document.querySelector("#blocks__meta")
const lastBlockheightElement = document.querySelector("#blocks__meta>.row.hash.item")
const lastBlockheight = lastBlockheightElement && lastBlockheightElement.getAttribute('data-id') || 0
blockMetas = blockMetas.filter(o=>o.header.height>lastBlockheight)
blockMetas.reverse().forEach(v=>{
const node = document.querySelector("#blocks__meta .hash.obj").cloneNode({deep: true});
node.classList.remove("obj")
node.classList.add("item")
node.setAttribute("data-id", v.header.height)
node.setAttribute('data-hash', v.header.data_hash)
node.querySelector('.head').textContent = "#"+v.header.height
node.querySelector('.desc>.transaction>span').textContent = v.header.num_txs
node.querySelector('.desc>.validator>.link').textContent = `loom${v.header.proposer_address}`
const since = node.querySelector('.desc>.since')
since.textContent = getSinceFrom(v.header.time)
since.setAttribute('data-since', v.header.time)
node.addEventListener("click", onBlockClickHandler)
list.prepend(node)
})
document.querySelectorAll("#blocks__meta .item").forEach((o,k)=>{
k>pageCnt && o.remove()
})
}
node.addEventListener("click", onBlockClickHandler) 클릭이벤트를 받을 수 있게 Handler도 붙인다.
const onBlockClickHandler = async e => {
const t = e.currentTarget
const dataId = t.getAttribute('data-id')
let tx = await aFetch(`${APIs.endpoint}${APIs.tx}0x${t.getAttribute('data-hash')}`)
document.querySelector("#blockDetail").classList.remove("chosen")
document.querySelector("#blockDetail").classList.add("chosen")
document.querySelector("#blockDetail .blockHeight").textContent = "#" + dataId
if (!tx) {
tx = {
hash: "",
tx_result: {
info: "",
data: "",
}
}
}
document.querySelector("#blockDetail .txDetail>.hash>.hash").textContent = tx.hash
document.querySelector("#blockDetail .txDetail>.result>.info").textContent = tx.tx_result.info
document.querySelector("#blockDetail .txDetail>.result>.data").textContent = tx.tx_result.data
}
내용은 뭘 보여줄까 하다가 tx 내용을 보여주기로 했다.
rpc 목록에 있는 tx 항목에 맞게 https://plasma.dappchains.com/rpc/tx?hash=0x4FADC98AB71EEC1E71384C5286B761B9AC58A6C1647C7DFF0886B8243BB9B062 이런 식으로 요청하면
{
"jsonrpc": "2.0",
"id": "",
"result": {
"hash": "4FADC98AB71EEC1E71384C5286B761B9AC58A6C1647C7DFF0886B8243BB9B062",
"height": "12841608",
"index": 0,
"tx_result": {
"data": "Y4urfueVKwei0sWkswDDa8rICBd7fxxYUnimqj+1Zc8=",
"info": "call.evm"
},
"tx": "ClsKVQgCElEKHwoHZGVmYXVsdBIUN1z+jN0iOvY2Yn9d3FmDht9zGkgSHwoHZGVmYXVsdBIUE5/Vz/WpnsQdMteVf2mWradVFxsaDQgBEgTe4RywGgMKAQAQ4KgEEkBO+30aq9YnK5dYEblZTo99TeDts53CQTahXm13Bxtg/BTyV+2fkZfC+hZusSuvgseO3S8hYfxQK9MAIcsex6wKGiCNeP1V3ZNXYw9Wib2fQ11XtB26/wa6wD7ByRBaXLlhfg=="
}
}
tx의 종류와 데이터가 나온다.
여기까지하고 일단 제출. 모집 글에 메일 주소 오타가 있어 두번 보냈는데 제대로 갔으려나 모르겠다.
다시 보내면서 모바일 대응도 좀 신경을 썼다.
하다보니 요런 모양이 나왔다. (룩을 손봐준 splex7 께 감사!)
아직 해야할 것이 많은데 피드백 보고 계속할지 생각해봐야겠다.
소스 저장소는 이쪽. 포크, 풀리퀘 모두 환영합니다.