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" .}}