diff --git a/api/types.go b/api/types.go index 8e8a1aca..b605bd30 100644 --- a/api/types.go +++ b/api/types.go @@ -46,6 +46,20 @@ type Tx struct { Size int `json:"size,omitempty"` ValueIn float64 `json:"valueIn"` Fees float64 `json:"fees"` - CoinShortcut string `json:"coinShortcut"` WithSpends bool `json:"withSpends,omitempty"` } + +type Address struct { + AddrStr string `json:"addrStr"` + Balance float64 `json:"balance"` + BalanceSat int64 `json:"balanceSat"` + TotalReceived float64 `json:"totalReceived"` + TotalReceivedSat int64 `json:"totalReceivedSat"` + TotalSent float64 `json:"totalSent"` + TotalSentSat int64 `json:"totalSentSat"` + UnconfirmedBalance float64 `json:"unconfirmedBalance"` + UnconfirmedBalanceSat int64 `json:"unconfirmedBalanceSat"` + UnconfirmedTxApperances int `json:"unconfirmedTxApperances"` + TxApperances int `json:"txApperances"` + Transactions []*Tx `json:"transactions"` +} diff --git a/api/worker.go b/api/worker.go index 0dc9c983..9d74b9bb 100644 --- a/api/worker.go +++ b/api/worker.go @@ -4,6 +4,8 @@ import ( "blockbook/bchain" "blockbook/common" "blockbook/db" + + "github.com/golang/glog" ) // Worker is handle to api worker @@ -91,7 +93,6 @@ func (w *Worker) GetTransaction(txid string, bestheight uint32, spendingTx bool) Blockhash: blockhash, Blockheight: int(height), Blocktime: bchainTx.Blocktime, - CoinShortcut: w.is.CoinShortcut, Confirmations: bchainTx.Confirmations, Fees: fees, Locktime: bchainTx.LockTime, @@ -106,3 +107,104 @@ func (w *Worker) GetTransaction(txid string, bestheight uint32, spendingTx bool) } return r, nil } + +func (s *Worker) getAddressTxids(address string, mempool bool) ([]string, error) { + var err error + txids := make([]string, 0) + if !mempool { + err = s.db.GetTransactions(address, 0, ^uint32(0), func(txid string, vout uint32, isOutput bool) error { + txids = append(txids, txid) + return nil + }) + if err != nil { + return nil, err + } + } else { + m, err := s.chain.GetMempoolTransactions(address) + if err != nil { + return nil, err + } + txids = append(txids, m...) + } + return txids, nil +} + +func (t *Tx) getAddrVoutValue(addrID string) float64 { + var val float64 + for _, vout := range t.Vout { + for _, a := range vout.ScriptPubKey.Addresses { + if a == addrID { + val += vout.Value + } + } + } + return val +} + +func (t *Tx) getAddrVinValue(addrID string) float64 { + var val float64 + for _, vin := range t.Vin { + if vin.Addr == addrID { + val += vin.Value + } + } + return val +} + +// GetAddress computes address value and gets transactions for given address +func (w *Worker) GetAddress(addrID string) (*Address, error) { + glog.Info(addrID, " start") + txc, err := w.getAddressTxids(addrID, false) + if err != nil { + return nil, err + } + txm, err := w.getAddressTxids(addrID, true) + if err != nil { + return nil, err + } + bestheight, _, err := w.db.GetBestBlock() + if err != nil { + return nil, err + } + txs := make([]*Tx, len(txc)+len(txm)) + txi := 0 + var uBal, bal, totRecv, totSent float64 + for _, tx := range txm { + tx, err := w.GetTransaction(tx, bestheight, false) + // mempool transaction may fail + if err != nil { + glog.Error("GetTransaction ", tx, ": ", err) + } else { + txs[txi] = tx + uBal = tx.getAddrVoutValue(addrID) - tx.getAddrVinValue(addrID) + txi++ + } + } + for i := len(txc) - 1; i >= 0; i-- { + tx, err := w.GetTransaction(txc[i], bestheight, false) + if err != nil { + return nil, err + } else { + txs[txi] = tx + totRecv += tx.getAddrVoutValue(addrID) + totSent += tx.getAddrVinValue(addrID) + txi++ + } + } + bal = totRecv - totSent + r := &Address{ + AddrStr: addrID, + Balance: bal, + BalanceSat: int64(bal*1E8 + 0.5), + TotalReceived: totRecv, + TotalReceivedSat: int64(totRecv*1E8 + 0.5), + TotalSent: totSent, + TotalSentSat: int64(totSent*1E8 + 0.5), + Transactions: txs[:txi], + TxApperances: len(txc), + UnconfirmedBalance: uBal, + UnconfirmedTxApperances: len(txm), + } + glog.Info(addrID, " finished") + return r, nil +} diff --git a/server/public.go b/server/public.go index 5476e8f2..89e0c1ca 100644 --- a/server/public.go +++ b/server/public.go @@ -34,6 +34,7 @@ type PublicServer struct { metrics *common.Metrics is *common.InternalState txTpl *template.Template + addressTpl *template.Template } // NewPublicServerS creates new public server http interface to blockbook and returns its handle @@ -80,26 +81,30 @@ func NewPublicServer(binding string, certFiles string, db *db.RocksDB, chain bch serveMux.HandleFunc(path+"address/", s.addressRedirect) // explorer serveMux.HandleFunc(path+"explorer/tx/", s.explorerTx) + serveMux.HandleFunc(path+"explorer/address/", s.explorerAddress) serveMux.Handle(path+"static/", http.StripPrefix("/static/", http.FileServer(http.Dir("./static/")))) // API calls serveMux.HandleFunc(path+"api/block-index/", s.apiBlockIndex) serveMux.HandleFunc(path+"api/tx/", s.apiTx) + serveMux.HandleFunc(path+"api/address/", s.apiAddress) // handle socket.io serveMux.Handle(path+"socket.io/", socketio.GetHandler()) // default handler serveMux.HandleFunc(path, s.index) - s.txTpl = parseTemplates() + s.txTpl, s.addressTpl = parseTemplates() return s, nil } -func parseTemplates() (txTpl *template.Template) { +func parseTemplates() (txTpl, addressTpl *template.Template) { templateFuncMap := template.FuncMap{ - "formatUnixTime": formatUnixTime, - "formatAmount": formatAmount, + "formatUnixTime": formatUnixTime, + "formatAmount": formatAmount, + "setTxToTemplateData": setTxToTemplateData, } txTpl = template.Must(template.New("tx").Funcs(templateFuncMap).ParseFiles("./static/templates/tx.html", "./static/templates/txdetail.html", "./static/templates/base.html")) + addressTpl = template.Must(template.New("address").Funcs(templateFuncMap).ParseFiles("./static/templates/address.html", "./static/templates/txdetail.html", "./static/templates/base.html")) return } @@ -175,6 +180,19 @@ func (s *PublicServer) addressRedirect(w http.ResponseWriter, r *http.Request) { } } +type TemplateData struct { + CoinName string + CoinShortcut string + Address *api.Address + AddrStr string + Tx *api.Tx +} + +func setTxToTemplateData(td *TemplateData, tx *api.Tx) *TemplateData { + td.Tx = tx + return td +} + func (s *PublicServer) explorerTx(w http.ResponseWriter, r *http.Request) { var tx *api.Tx if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 { @@ -191,17 +209,45 @@ func (s *PublicServer) explorerTx(w http.ResponseWriter, r *http.Request) { // temporarily reread the template on each request // to reflect changes during development - s.txTpl = parseTemplates() + s.txTpl, s.addressTpl = parseTemplates() - data := struct { - CoinName string - Specific *api.Tx - }{s.is.Coin, tx} + data := &TemplateData{ + CoinName: s.is.Coin, + CoinShortcut: s.is.CoinShortcut, + Tx: tx, + } if err := s.txTpl.ExecuteTemplate(w, "base.html", data); err != nil { glog.Error(err) } } +func (s *PublicServer) explorerAddress(w http.ResponseWriter, r *http.Request) { + var address *api.Address + var err error + if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 { + addrID := r.URL.Path[i+1:] + address, err = s.api.GetAddress(addrID) + if err != nil { + glog.Error(err) + } + } + w.Header().Set("Content-Type", "text/html; charset=utf-8") + + // temporarily reread the template on each request + // to reflect changes during development + s.txTpl, s.addressTpl = parseTemplates() + + data := &TemplateData{ + CoinName: s.is.Coin, + CoinShortcut: s.is.CoinShortcut, + AddrStr: address.AddrStr, + Address: address, + } + if err := s.addressTpl.ExecuteTemplate(w, "base.html", data); err != nil { + glog.Error(err) + } +} + type resAboutBlockbookPublic struct { Coin string `json:"coin"` Host string `json:"host"` @@ -288,3 +334,19 @@ func (s *PublicServer) apiTx(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(tx) } } + +func (s *PublicServer) apiAddress(w http.ResponseWriter, r *http.Request) { + var address *api.Address + var err error + if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 { + addrID := r.URL.Path[i+1:] + address, err = s.api.GetAddress(addrID) + if err != nil { + glog.Error(err) + } + } + if err == nil { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + json.NewEncoder(w).Encode(address) + } +} diff --git a/static/templates/address.html b/static/templates/address.html new file mode 100644 index 00000000..3a457703 --- /dev/null +++ b/static/templates/address.html @@ -0,0 +1,52 @@ +{{define "specific"}}{{$cs := .CoinShortcut}}{{$addr := .Address}}{{$data := .}} +

Address + {{formatAmount $addr.Balance}} {{$cs}} +

+
+ {{$addr.AddrStr}} +
+

Confirmed

+
+ + + + + + + + + + + + + + + + + + + +
Total Received{{formatAmount $addr.TotalReceived}} {{$cs}}
Total Sent{{formatAmount $addr.TotalSent}} {{$cs}}
Final Balance{{formatAmount $addr.Balance}} {{$cs}}
No. Transactions{{$addr.TxApperances}}
+
+{{if $addr.UnconfirmedTxApperances}} +

Unconfirmed

+
+ + + + + + + + + + + +
Unconfirmed Balance{{formatAmount $addr.UnconfirmedBalance}} {{$cs}}
No. Transactions{{$addr.UnconfirmedTxApperances}}
+
+{{end}} {{if $addr.Transactions}} +

Transactions

+
+ {{range $tx := $addr.Transactions}}{{$data := setTxToTemplateData $data $tx}}{{template "txdetail" $data }}{{end}} +
+{{end}} {{end}} \ No newline at end of file diff --git a/static/templates/base.html b/static/templates/base.html index 2bf99835..5ab2fb3e 100644 --- a/static/templates/base.html +++ b/static/templates/base.html @@ -48,7 +48,7 @@
- {{template "specific" .Specific}} + {{template "specific" .}}