diff --git a/api/types.go b/api/types.go index a2ddde4a..e0e562ca 100644 --- a/api/types.go +++ b/api/types.go @@ -337,3 +337,16 @@ type SystemInfo struct { Blockbook *BlockbookInfo `json:"blockbook"` Backend *bchain.ChainInfo `json:"backend"` } + +// MempoolTxid contains information about a transaction in mempool +type MempoolTxid struct { + Time int64 `json:"time"` + Txid string `json:"txid"` +} + +// MempoolTxids contains a list of mempool txids with paging information +type MempoolTxids struct { + Paging + Mempool []MempoolTxid `json:"mempool"` + MempoolSize int `json:"mempoolSize"` +} diff --git a/api/worker.go b/api/worker.go index 806bcf2c..b19b1e11 100644 --- a/api/worker.go +++ b/api/worker.go @@ -1070,3 +1070,26 @@ func (w *Worker) GetSystemInfo(internal bool) (*SystemInfo, error) { glog.Info("GetSystemInfo finished in ", time.Since(start)) return &SystemInfo{bi, ci}, nil } + +// GetMempool returns a page of mempool txids +func (w *Worker) GetMempool(page int, itemsOnPage int) (*MempoolTxids, error) { + page-- + if page < 0 { + page = 0 + } + entries := w.mempool.GetAllEntries() + pg, from, to, page := computePaging(len(entries), page, itemsOnPage) + r := &MempoolTxids{ + Paging: pg, + MempoolSize: len(entries), + } + r.Mempool = make([]MempoolTxid, to-from) + for i := from; i < to; i++ { + entry := &entries[i] + r.Mempool[i-from] = MempoolTxid{ + Txid: entry.Txid, + Time: int64(entry.Time), + } + } + return r, nil +} diff --git a/bchain/coins/blockchain.go b/bchain/coins/blockchain.go index 6f1913fc..2567c77f 100644 --- a/bchain/coins/blockchain.go +++ b/bchain/coins/blockchain.go @@ -311,3 +311,8 @@ func (c *mempoolWithMetrics) GetAddrDescTransactions(addrDesc bchain.AddressDesc defer func(s time.Time) { c.observeRPCLatency("GetMempoolTransactionsForAddrDesc", s, err) }(time.Now()) return c.mempool.GetAddrDescTransactions(addrDesc) } + +func (c *mempoolWithMetrics) GetAllEntries() (v bchain.MempoolTxidEntries) { + defer func(s time.Time) { c.observeRPCLatency("GetAllEntries", s, nil) }(time.Now()) + return c.mempool.GetAllEntries() +} diff --git a/bchain/mempool_bitcoin_type.go b/bchain/mempool_bitcoin_type.go index 887ecee1..8dba0e5b 100644 --- a/bchain/mempool_bitcoin_type.go +++ b/bchain/mempool_bitcoin_type.go @@ -1,6 +1,7 @@ package bchain import ( + "sort" "sync" "time" @@ -221,3 +222,35 @@ func (m *MempoolBitcoinType) Resync() (int, error) { glog.Info("mempool: resync finished in ", time.Since(start), ", ", len(m.txEntries), " transactions in mempool") return len(m.txEntries), nil } + +func (a MempoolTxidEntries) Len() int { return len(a) } +func (a MempoolTxidEntries) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a MempoolTxidEntries) Less(i, j int) bool { + // if the Time is equal, sort by txid to make the order defined + hi := a[i].Time + hj := a[j].Time + if hi == hj { + return a[i].Txid > a[j].Txid + } + // order in reverse + return hi > hj +} + +func getAllEntries(txEntries map[string]txEntry) MempoolTxidEntries { + a := make(MempoolTxidEntries, len(txEntries)) + i := 0 + for txid, entry := range txEntries { + a[i] = MempoolTxidEntry{ + Txid: txid, + Time: entry.time, + } + i++ + } + sort.Sort(a) + return a +} + +// GetAllEntries returns all mempool entries sorted by fist seen time in descending order +func (m *MempoolBitcoinType) GetAllEntries() MempoolTxidEntries { + return getAllEntries(m.txEntries) +} diff --git a/bchain/mempool_ethereum_type.go b/bchain/mempool_ethereum_type.go index c6033de6..e81744d3 100644 --- a/bchain/mempool_ethereum_type.go +++ b/bchain/mempool_ethereum_type.go @@ -137,3 +137,8 @@ func (m *MempoolEthereumType) Resync() (int, error) { glog.Info("Mempool: resync finished in ", time.Since(start), ", ", len(m.txEntries), " transactions in mempool") return len(m.txEntries), nil } + +// GetAllEntries returns all mempool entries sorted by fist seen time in descending order +func (m *MempoolEthereumType) GetAllEntries() MempoolTxidEntries { + return getAllEntries(m.txEntries) +} diff --git a/bchain/types.go b/bchain/types.go index 368965fa..58b1acf9 100644 --- a/bchain/types.go +++ b/bchain/types.go @@ -185,6 +185,15 @@ type Erc20Transfer struct { Tokens big.Int } +// MempoolTxidEntry contains mempool txid with first seen time +type MempoolTxidEntry struct { + Txid string + Time uint32 +} + +// MempoolTxidEntries is array of MempoolTxidEntry +type MempoolTxidEntries []MempoolTxidEntry + // OnNewBlockFunc is used to send notification about a new block type OnNewBlockFunc func(hash string, height uint32) @@ -281,4 +290,5 @@ type Mempool interface { Resync() (int, error) GetTransactions(address string) ([]Outpoint, error) GetAddrDescTransactions(addrDesc AddressDescriptor) ([]Outpoint, error) + GetAllEntries() MempoolTxidEntries } diff --git a/server/public.go b/server/public.go index 7543ec2a..60ca2e53 100644 --- a/server/public.go +++ b/server/public.go @@ -26,6 +26,7 @@ import ( const txsOnPage = 25 const blocksOnPage = 50 +const mempoolTxsOnPage = 50 const txsInAPI = 1000 const ( @@ -139,6 +140,7 @@ func (s *PublicServer) ConnectFullPublicInterface() { serveMux.HandleFunc(path+"block/", s.htmlTemplateHandler(s.explorerBlock)) serveMux.HandleFunc(path+"spending/", s.htmlTemplateHandler(s.explorerSpendingTx)) serveMux.HandleFunc(path+"sendtx", s.htmlTemplateHandler(s.explorerSendTx)) + serveMux.HandleFunc(path+"mempool", s.htmlTemplateHandler(s.explorerMempool)) } else { // redirect to wallet requests for tx and address, possibly to external site serveMux.HandleFunc(path+"tx/", s.txRedirect) @@ -384,6 +386,7 @@ const ( blocksTpl blockTpl sendTransactionTpl + mempoolTpl tplCount ) @@ -402,6 +405,7 @@ type TemplateData struct { Blocks *api.Blocks Block *api.Block Info *api.SystemInfo + MempoolTxids *api.MempoolTxids Page int PrevPage int NextPage int @@ -477,6 +481,7 @@ func (s *PublicServer) parseTemplates() []*template.Template { t[blockTpl] = createTemplate("./static/templates/block.html", "./static/templates/txdetail.html", "./static/templates/paging.html", "./static/templates/base.html") } t[xpubTpl] = createTemplate("./static/templates/xpub.html", "./static/templates/txdetail.html", "./static/templates/paging.html", "./static/templates/base.html") + t[mempoolTpl] = createTemplate("./static/templates/mempool.html", "./static/templates/paging.html", "./static/templates/base.html") return t } @@ -798,6 +803,25 @@ func (s *PublicServer) explorerSendTx(w http.ResponseWriter, r *http.Request) (t return sendTransactionTpl, data, nil } +func (s *PublicServer) explorerMempool(w http.ResponseWriter, r *http.Request) (tpl, *TemplateData, error) { + var mempoolTxids *api.MempoolTxids + var err error + s.metrics.ExplorerViews.With(common.Labels{"action": "mempool"}).Inc() + page, ec := strconv.Atoi(r.URL.Query().Get("page")) + if ec != nil { + page = 0 + } + mempoolTxids, err = s.api.GetMempool(page, mempoolTxsOnPage) + if err != nil { + return errorTpl, nil, err + } + data := s.newTemplateData() + data.MempoolTxids = mempoolTxids + data.Page = mempoolTxids.Page + data.PagingRange, data.PrevPage, data.NextPage = getPagingRange(mempoolTxids.Page, mempoolTxids.TotalPages) + return mempoolTpl, data, nil +} + func getPagingRange(page int, total int) ([]int, int, int) { // total==-1 means total is unknown, show only prev/next buttons if total >= 0 && total < 2 { diff --git a/static/css/main.css b/static/css/main.css index d54f9841..c0bf1ee2 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -268,6 +268,11 @@ table.data-table table.data-table th { margin: 0; } +.h-container h5 { + margin-top: 6px; + margin-bottom: 0; +} + .page-link { color: #428bca; } diff --git a/static/templates/index.html b/static/templates/index.html index f6638adc..5e275fc3 100644 --- a/static/templates/index.html +++ b/static/templates/index.html @@ -43,6 +43,10 @@
| Transaction | +First Seen Time | +
|---|---|
| {{$tx.Txid}} | +{{formatUnixTime $tx.Time}} | +