loom network의 blockchain explorer를 만들어 보자.

in #kr-dev5 years ago (edited)

한동안 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의 종류와 데이터가 나온다.

여기까지하고 일단 제출. 모집 글에 메일 주소 오타가 있어 두번 보냈는데 제대로 갔으려나 모르겠다.
다시 보내면서 모바일 대응도 좀 신경을 썼다.
screenshot
하다보니 요런 모양이 나왔다. (룩을 손봐준 splex7 께 감사!)

아직 해야할 것이 많은데 피드백 보고 계속할지 생각해봐야겠다.

소스 저장소는 이쪽. 포크, 풀리퀘 모두 환영합니다.