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

in kr-dev •  5 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 께 감사!)

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

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

Authors get paid when people like you upvote their post.
If you enjoyed what you read here, create your account today and start earning FREE STEEM!