Add view of block to explorer
parent
699f259e3d
commit
d87d52b2fd
|
@ -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"`
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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}}
|
Loading…
Reference in New Issue