Add view of block to explorer

pull/56/head
Martin Boehm 2018-09-17 18:28:08 +02:00
parent 699f259e3d
commit d87d52b2fd
8 changed files with 258 additions and 21 deletions

View File

@ -104,6 +104,13 @@ type Blocks struct {
Blocks []db.BlockInfo `json:"blocks"`
}
type Block struct {
Paging
bchain.BlockInfo
TxCount int `json:"TxCount"`
Transactions []*Tx `json:"txs,omitempty"`
}
type BlockbookInfo struct {
Coin string `json:"coin"`
Host string `json:"host"`

View File

@ -7,6 +7,7 @@ import (
"bytes"
"fmt"
"math/big"
"strconv"
"time"
"github.com/golang/glog"
@ -466,6 +467,64 @@ func (w *Worker) GetBlocks(page int, blocksOnPage int) (*Blocks, error) {
return r, nil
}
// GetBlock returns paged data about block
func (w *Worker) GetBlock(bid string, page int, txsOnPage int) (*Block, error) {
start := time.Now()
page--
if page < 0 {
page = 0
}
var hash string
height, err := strconv.Atoi(bid)
if err == nil && height < int(^uint32(0)) {
hash, err = w.db.GetBlockHash(uint32(height))
} else {
hash = bid
}
bi, err := w.chain.GetBlockInfo(hash)
if err != nil {
if err == bchain.ErrBlockNotFound {
return nil, NewApiError("Block not found", true)
}
return nil, NewApiError(fmt.Sprintf("Block not found, %v", err), true)
}
dbi := &db.BlockInfo{
Hash: bi.Hash,
Height: bi.Height,
Time: bi.Time,
}
txCount := len(bi.Txids)
bestheight, _, err := w.db.GetBestBlock()
if err != nil {
return nil, errors.Annotatef(err, "GetBestBlock")
}
pg, from, to, page := computePaging(txCount, page, txsOnPage)
glog.Info("GetBlock ", bid, ", page ", page, " finished in ", time.Since(start))
txs := make([]*Tx, to-from)
txi := 0
for i := from; i < to; i++ {
txid := bi.Txids[i]
ta, err := w.db.GetTxAddresses(txid)
if err != nil {
return nil, errors.Annotatef(err, "GetTxAddresses %v", txid)
}
if ta == nil {
glog.Warning("DB inconsistency: tx ", txid, ": not found in txAddresses")
continue
}
txs[txi] = w.txFromTxAddress(txid, ta, dbi, bestheight)
txi++
}
txs = txs[:txi]
bi.Txids = nil
return &Block{
Paging: pg,
BlockInfo: *bi,
TxCount: txCount,
Transactions: txs,
}, nil
}
// GetSystemInfo returns information about system
func (w *Worker) GetSystemInfo() (*SystemInfo, error) {
start := time.Now()

View File

@ -162,6 +162,11 @@ func (c *blockChainWithMetrics) GetBlock(hash string, height uint32) (v *bchain.
return c.b.GetBlock(hash, height)
}
func (c *blockChainWithMetrics) GetBlockInfo(hash string) (v *bchain.BlockInfo, err error) {
defer func(s time.Time) { c.observeRPCLatency("GetBlockInfo", s, err) }(time.Now())
return c.b.GetBlockInfo(hash)
}
func (c *blockChainWithMetrics) GetMempool() (v []string, err error) {
defer func(s time.Time) { c.observeRPCLatency("GetMempool", s, err) }(time.Now())
return c.b.GetMempool()

View File

@ -287,9 +287,14 @@ type ResGetBlockRaw struct {
Result string `json:"result"`
}
type BlockThin struct {
bchain.BlockHeader
Txids []string `json:"tx"`
}
type ResGetBlockThin struct {
Error *bchain.RPCError `json:"error"`
Result bchain.ThinBlock `json:"result"`
Result BlockThin `json:"result"`
}
type ResGetBlockFull struct {
@ -297,6 +302,11 @@ type ResGetBlockFull struct {
Result bchain.Block `json:"result"`
}
type ResGetBlockInfo struct {
Error *bchain.RPCError `json:"error"`
Result bchain.BlockInfo `json:"result"`
}
// getrawtransaction
type CmdGetRawTransaction struct {
@ -536,6 +546,28 @@ func (b *BitcoinRPC) GetBlock(hash string, height uint32) (*bchain.Block, error)
return block, nil
}
// GetBlockInfo returns extended header (more info than in bchain.BlockHeader) with a list of txids
func (b *BitcoinRPC) GetBlockInfo(hash string) (*bchain.BlockInfo, error) {
glog.V(1).Info("rpc: getblock (verbosity=1) ", hash)
res := ResGetBlockInfo{}
req := CmdGetBlock{Method: "getblock"}
req.Params.BlockHash = hash
req.Params.Verbosity = 1
err := b.Call(&req, &res)
if err != nil {
return nil, errors.Annotatef(err, "hash %v", hash)
}
if res.Error != nil {
if isErrBlockNotFound(res.Error) {
return nil, bchain.ErrBlockNotFound
}
return nil, errors.Annotatef(res.Error, "hash %v", hash)
}
return &res.Result, nil
}
// GetBlockWithoutHeader is an optimization - it does not call GetBlockHeader to get prev, next hashes
// instead it sets to header only block hash and height passed in parameters
func (b *BitcoinRPC) GetBlockWithoutHeader(hash string, height uint32) (*bchain.Block, error) {

View File

@ -424,6 +424,12 @@ func (b *EthereumRPC) GetBlock(hash string, height uint32) (*bchain.Block, error
return &bbk, nil
}
// GetBlockInfo returns extended header (more info than in bchain.BlockHeader) with a list of txids
func (b *EthereumRPC) GetBlockInfo(hash string) (*bchain.BlockInfo, error) {
// TODO - implement
return nil, errors.New("Not implemented yet")
}
// GetTransactionForMempool returns a transaction by the transaction ID.
// It could be optimized for mempool, i.e. without block time and confirmations
func (b *EthereumRPC) GetTransactionForMempool(txid string) (*bchain.Tx, error) {

View File

@ -71,11 +71,7 @@ type Block struct {
Txs []Tx `json:"tx"`
}
type ThinBlock struct {
BlockHeader
Txids []string `json:"tx"`
}
// BlockHeader contains limited data (as needed for indexing) from backend block header
type BlockHeader struct {
Hash string `json:"hash"`
Prev string `json:"previousblockhash"`
@ -86,6 +82,17 @@ type BlockHeader struct {
Time int64 `json:"time,omitempty"`
}
// BlockInfo contains extended block header data and a list of block txids
type BlockInfo struct {
BlockHeader
Version int64 `json:"version"`
MerkleRoot string `json:"merkleroot"`
Nonce uint64 `json:"nonce"`
Bits string `json:"bits"`
Difficulty float64 `json:"difficulty"`
Txids []string `json:"tx,omitempty"`
}
type MempoolEntry struct {
Size uint32 `json:"size"`
FeeSat big.Int
@ -156,6 +163,7 @@ type BlockChain interface {
GetBlockHash(height uint32) (string, error)
GetBlockHeader(hash string) (*BlockHeader, error)
GetBlock(hash string, height uint32) (*Block, error)
GetBlockInfo(hash string) (*BlockInfo, error)
GetMempool() ([]string, error)
GetTransaction(txid string) (*Tx, error)
GetTransactionForMempool(txid string) (*Tx, error)

View File

@ -89,11 +89,13 @@ func NewPublicServer(binding string, certFiles string, db *db.RocksDB, chain bch
serveMux.HandleFunc(path+"explorer/address/", s.htmlTemplateHandler(s.explorerAddress))
serveMux.HandleFunc(path+"explorer/search/", s.htmlTemplateHandler(s.explorerSearch))
serveMux.HandleFunc(path+"explorer/blocks", s.htmlTemplateHandler(s.explorerBlocks))
serveMux.HandleFunc(path+"explorer/block/", s.htmlTemplateHandler(s.explorerBlock))
serveMux.Handle(path+"static/", http.StripPrefix("/static/", http.FileServer(http.Dir("./static/"))))
// API calls
serveMux.HandleFunc(path+"api/block-index/", s.jsonHandler(s.apiBlockIndex))
serveMux.HandleFunc(path+"api/tx/", s.jsonHandler(s.apiTx))
serveMux.HandleFunc(path+"api/address/", s.jsonHandler(s.apiAddress))
serveMux.HandleFunc(path+"api/block/", s.jsonHandler(s.apiBlock))
serveMux.HandleFunc(path+"api/", s.jsonHandler(s.apiIndex))
// handle socket.io
serveMux.Handle(path+"socket.io/", socketio.GetHandler())
@ -281,6 +283,7 @@ const (
txTpl
addressTpl
blocksTpl
blockTpl
tplCount
)
@ -293,6 +296,7 @@ type TemplateData struct {
Tx *api.Tx
Error *api.ApiError
Blocks *api.Blocks
Block *api.Block
Info *api.SystemInfo
Page int
PrevPage int
@ -314,6 +318,7 @@ func parseTemplates() []*template.Template {
t[txTpl] = template.Must(template.New("tx").Funcs(templateFuncMap).ParseFiles("./static/templates/tx.html", "./static/templates/txdetail.html", "./static/templates/base.html"))
t[addressTpl] = template.Must(template.New("address").Funcs(templateFuncMap).ParseFiles("./static/templates/address.html", "./static/templates/txdetail.html", "./static/templates/paging.html", "./static/templates/base.html"))
t[blocksTpl] = template.Must(template.New("blocks").Funcs(templateFuncMap).ParseFiles("./static/templates/blocks.html", "./static/templates/paging.html", "./static/templates/base.html"))
t[blockTpl] = template.Must(template.New("block").Funcs(templateFuncMap).ParseFiles("./static/templates/block.html", "./static/templates/txdetail.html", "./static/templates/paging.html", "./static/templates/base.html"))
return t
}
@ -396,6 +401,27 @@ func (s *PublicServer) explorerBlocks(w http.ResponseWriter, r *http.Request) (t
return blocksTpl, data, nil
}
func (s *PublicServer) explorerBlock(w http.ResponseWriter, r *http.Request) (tpl, *TemplateData, error) {
var block *api.Block
var err error
s.metrics.ExplorerViews.With(common.Labels{"action": "block"}).Inc()
if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 {
page, ec := strconv.Atoi(r.URL.Query().Get("page"))
if ec != nil {
page = 0
}
block, err = s.api.GetBlock(r.URL.Path[i+1:], page, txsOnPage)
if err != nil {
return errorTpl, nil, err
}
}
data := s.newTemplateData()
data.Block = block
data.Page = block.Page
data.PagingRange, data.PrevPage, data.NextPage = getPagingRange(block.Page, block.TotalPages)
return blockTpl, data, nil
}
func (s *PublicServer) explorerIndex(w http.ResponseWriter, r *http.Request) (tpl, *TemplateData, error) {
var si *api.SystemInfo
var err error
@ -413,29 +439,31 @@ func (s *PublicServer) explorerSearch(w http.ResponseWriter, r *http.Request) (t
q := strings.TrimSpace(r.URL.Query().Get("q"))
var tx *api.Tx
var address *api.Address
var block *api.Block
var bestheight uint32
var err error
s.metrics.ExplorerViews.With(common.Labels{"action": "search"}).Inc()
if len(q) > 0 {
if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 {
bestheight, _, err := s.db.GetBestBlock()
block, err = s.api.GetBlock(q, 0, 1)
if err == nil {
http.Redirect(w, r, joinURL("/explorer/block/", block.Hash), 302)
return noTpl, nil, nil
}
bestheight, _, err = s.db.GetBestBlock()
if err == nil {
tx, err = s.api.GetTransaction(q, bestheight, false)
if err == nil {
tx, err = s.api.GetTransaction(q, bestheight, false)
if err == nil {
http.Redirect(w, r, joinURL("/explorer/tx/", tx.Txid), 302)
return noTpl, nil, nil
}
}
address, err = s.api.GetAddress(q, 0, 1, true)
if err == nil {
http.Redirect(w, r, joinURL("/explorer/address/", address.AddrStr), 302)
http.Redirect(w, r, joinURL("/explorer/tx/", tx.Txid), 302)
return noTpl, nil, nil
}
}
address, err = s.api.GetAddress(q, 0, 1, true)
if err == nil {
http.Redirect(w, r, joinURL("/explorer/address/", address.AddrStr), 302)
return noTpl, nil, nil
}
}
if err == nil {
err = api.NewApiError(fmt.Sprintf("No matching records found for '%v'", q), true)
}
return errorTpl, nil, err
return errorTpl, nil, api.NewApiError(fmt.Sprintf("No matching records found for '%v'", q), true)
}
func getPagingRange(page int, total int) ([]int, int, int) {
@ -550,3 +578,17 @@ func (s *PublicServer) apiAddress(r *http.Request) (interface{}, error) {
}
return address, err
}
func (s *PublicServer) apiBlock(r *http.Request) (interface{}, error) {
var block *api.Block
var err error
s.metrics.ExplorerViews.With(common.Labels{"action": "api-block"}).Inc()
if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 {
page, ec := strconv.Atoi(r.URL.Query().Get("page"))
if ec != nil {
page = 0
}
block, err = s.api.GetBlock(r.URL.Path[i+1:], page, txsInAPI)
}
return block, err
}

View File

@ -0,0 +1,78 @@
{{define "specific"}}{{$cs := .CoinShortcut}}{{$b := .Block}}{{$data := .}}
<h1>Block {{$b.Height}}</h1>
<div class="alert alert-data">
<span class="ellipsis data">{{$b.Hash}}</span>
</div>
<div class="h-container">
<h3 class="h-container-6">Summary</h3>
<nav class="h-container-6">
<ul class="pagination justify-content-end">
{{- if $b.Prev}}<li class="page-item"><a class="page-link" href="/explorer/block/{{$b.Prev}}">Previous Block</a></li>{{end -}}
{{- if $b.Next}}<li class="page-item"><a class="page-link" href="/explorer/block/{{$b.Next}}">Next Block</a></li>{{end -}}
</ul>
</nav>
</div>
<div class="data-div row">
<div class="col-md-6">
<table class="table data-table">
<tbody>
<tr>
<td style="width: 25%;">Transactions</td>
<td class="data">{{$b.TxCount}}</td>
</tr>
<tr>
<td>Height</td>
<td class="data">{{$b.Height}}</td>
</tr>
<tr>
<td>Confirmations</td>
<td class="data">{{$b.Confirmations}}</td>
</tr>
<tr>
<td>Timestamp</td>
<td class="data">{{formatUnixTime $b.Time}}</td>
</tr>
<tr>
<td>Size (bytes)</td>
<td class="data">{{$b.Size}}</td>
</tr>
</tbody>
</table>
</div>
<div class="col-md-6">
<table class="table data-table">
<tbody>
<tr>
<td style="width: 25%;">Version</td>
<td class="data">{{$b.Version}}</td>
</tr>
<tr>
<td>Merkle Root</td>
<td class="data ellipsis">{{$b.MerkleRoot}}</td>
</tr>
<tr>
<td>Nonce</td>
<td class="data">{{$b.Nonce}}</td>
</tr>
<tr>
<td>Bits</td>
<td class="data">{{$b.Bits}}</td>
</tr>
<tr>
<td>Difficulty</td>
<td class="data">{{$b.Difficulty}}</td>
</tr>
</tbody>
</table>
</div>
</div>
{{- if $b.Transactions -}}
<div class="h-container">
<h3 class="h-container-6">Transactions</h3>
<nav class="h-container-6">{{template "paging" $data}}</nav>
</div>
<div class="data-div">
{{- range $tx := $b.Transactions}}{{$data := setTxToTemplateData $data $tx}}{{template "txdetail" $data}}{{end -}}
</div>
<nav>{{template "paging" $data }}</nav>
{{end}}{{end}}