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