From d87d52b2fdb215f402cae4799f87a828f476fb14 Mon Sep 17 00:00:00 2001 From: Martin Boehm Date: Mon, 17 Sep 2018 18:28:08 +0200 Subject: [PATCH] Add view of block to explorer --- api/types.go | 7 +++ api/worker.go | 59 +++++++++++++++++++++++++ bchain/coins/blockchain.go | 5 +++ bchain/coins/btc/bitcoinrpc.go | 34 ++++++++++++++- bchain/coins/eth/ethrpc.go | 6 +++ bchain/types.go | 18 +++++--- server/public.go | 72 ++++++++++++++++++++++++------- static/templates/block.html | 78 ++++++++++++++++++++++++++++++++++ 8 files changed, 258 insertions(+), 21 deletions(-) create mode 100644 static/templates/block.html diff --git a/api/types.go b/api/types.go index 83b3287a..60952a71 100644 --- a/api/types.go +++ b/api/types.go @@ -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"` diff --git a/api/worker.go b/api/worker.go index 861b55e6..132d40aa 100644 --- a/api/worker.go +++ b/api/worker.go @@ -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() diff --git a/bchain/coins/blockchain.go b/bchain/coins/blockchain.go index 89e84de6..8ae42d94 100644 --- a/bchain/coins/blockchain.go +++ b/bchain/coins/blockchain.go @@ -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() diff --git a/bchain/coins/btc/bitcoinrpc.go b/bchain/coins/btc/bitcoinrpc.go index de4c94d6..576c35f9 100644 --- a/bchain/coins/btc/bitcoinrpc.go +++ b/bchain/coins/btc/bitcoinrpc.go @@ -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) { diff --git a/bchain/coins/eth/ethrpc.go b/bchain/coins/eth/ethrpc.go index da97c3d3..83be98bf 100644 --- a/bchain/coins/eth/ethrpc.go +++ b/bchain/coins/eth/ethrpc.go @@ -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) { diff --git a/bchain/types.go b/bchain/types.go index bdb47a2a..84c87f99 100644 --- a/bchain/types.go +++ b/bchain/types.go @@ -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) diff --git a/server/public.go b/server/public.go index dbcb420f..302a2cf5 100644 --- a/server/public.go +++ b/server/public.go @@ -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 +} diff --git a/static/templates/block.html b/static/templates/block.html new file mode 100644 index 00000000..08704e2b --- /dev/null +++ b/static/templates/block.html @@ -0,0 +1,78 @@ +{{define "specific"}}{{$cs := .CoinShortcut}}{{$b := .Block}}{{$data := .}} +

Block {{$b.Height}}

+
+ {{$b.Hash}} +
+
+

Summary

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + +
Transactions{{$b.TxCount}}
Height{{$b.Height}}
Confirmations{{$b.Confirmations}}
Timestamp{{formatUnixTime $b.Time}}
Size (bytes){{$b.Size}}
+
+
+ + + + + + + + + + + + + + + + + + + + + + + +
Version{{$b.Version}}
Merkle Root{{$b.MerkleRoot}}
Nonce{{$b.Nonce}}
Bits{{$b.Bits}}
Difficulty{{$b.Difficulty}}
+
+
+{{- if $b.Transactions -}} +
+

Transactions

+ +
+
+ {{- range $tx := $b.Transactions}}{{$data := setTxToTemplateData $data $tx}}{{template "txdetail" $data}}{{end -}} +
+ +{{end}}{{end}} \ No newline at end of file