Add initial implementation of address explorer

This commit is contained in:
Martin Boehm 2018-06-30 02:02:16 +02:00
parent 20643756e5
commit 7e68630377
7 changed files with 270 additions and 36 deletions

View File

@ -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"`
}

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -0,0 +1,52 @@
{{define "specific"}}{{$cs := .CoinShortcut}}{{$addr := .Address}}{{$data := .}}
<h1>Address
<small class="text-muted">{{formatAmount $addr.Balance}} {{$cs}}</small>
</h1>
<div class="alert alert-data">
<span class="ellipsis data">{{$addr.AddrStr}}</span>
</div>
<h3>Confirmed</h3>
<div class="data-div">
<table class="table data-table">
<tbody>
<tr>
<td style="width: 25%;">Total Received</td>
<td class="data">{{formatAmount $addr.TotalReceived}} {{$cs}}</td>
</tr>
<tr>
<td>Total Sent</td>
<td class="data">{{formatAmount $addr.TotalSent}} {{$cs}}</td>
</tr>
<tr>
<td>Final Balance</td>
<td class="data">{{formatAmount $addr.Balance}} {{$cs}}</td>
</tr>
<tr>
<td>No. Transactions</td>
<td class="data">{{$addr.TxApperances}}</td>
</tr>
</tbody>
</table>
</div>
{{if $addr.UnconfirmedTxApperances}}
<h3>Unconfirmed</h3>
<div class="data-div">
<table class="table data-table">
<tbody>
<tr>
<td style="width: 25%;">Unconfirmed Balance</td>
<td class="data">{{formatAmount $addr.UnconfirmedBalance}} {{$cs}}</td>
</tr>
<tr>
<td>No. Transactions</td>
<td class="data">{{$addr.UnconfirmedTxApperances}}</td>
</tr>
</tbody>
</table>
</div>
{{end}} {{if $addr.Transactions}}
<h3>Transactions</h3>
<div class="data-div">
{{range $tx := $addr.Transactions}}{{$data := setTxToTemplateData $data $tx}}{{template "txdetail" $data }}{{end}}
</div>
{{end}} {{end}}

View File

@ -48,7 +48,7 @@
</header>
<main id="wrap">
<div class="container">
{{template "specific" .Specific}}
{{template "specific" .}}
</div>
</main>
<footer id="footer" class="footer">

View File

@ -1,40 +1,44 @@
{{define "specific"}}
{{define "specific"}}{{$cs := .CoinShortcut}}{{$tx := .Tx}}
<h1>Transaction</h1>
<div class="alert alert-data">
<span class="ellipsis data">{{.Txid}}</span>
<span class="ellipsis data">{{$tx.Txid}}</span>
</div>
<h3>Summary</h3>
<div class="data-div">
<table class="table data-table">
<tbody>
{{if .Confirmations}}<tr>
{{if $tx.Confirmations}}
<tr>
<td style="width: 25%;">Mined Time</td>
<td class="data">{{formatUnixTime .Blocktime}}</td>
<td class="data">{{formatUnixTime $tx.Blocktime}}</td>
</tr>{{end}}
<tr>
<td style="width: 25%;">In Block</td>
<td class="ellipsis data">{{if .Confirmations}}{{.Blockhash}}{{else}}Unconfirmed{{end}}</td>
<td class="ellipsis data">{{if $tx.Confirmations}}{{$tx.Blockhash}}{{else}}Unconfirmed{{end}}</td>
</tr>
{{if .Confirmations}}<tr>
{{if $tx.Confirmations}}
<tr>
<td>In Block Height</td>
<td class="data">{{.Blockheight}}</td>
<td class="data">{{$tx.Blockheight}}</td>
</tr>{{end}}
<tr>
<td>Total Input</td>
<td class="data">{{formatAmount .ValueIn}} {{.CoinShortcut}}</td>
<td class="data">{{formatAmount $tx.ValueIn}} {{$cs}}</td>
</tr>
<tr>
<td>Total Output</td>
<td class="data">{{formatAmount .ValueOut}} {{.CoinShortcut}}</td>
<td class="data">{{formatAmount $tx.ValueOut}} {{$cs}}</td>
</tr>
{{if .Fees}}<tr>
{{if $tx.Fees}}
<tr>
<td>Fees</td>
<td class="data">{{formatAmount .Fees}} {{.CoinShortcut}}</td>
<td class="data">{{formatAmount $tx.Fees}} {{$cs}}</td>
</tr>{{end}}
</tbody>
</table>
</div>
<h3>Details</h3>
<div class="data-div">
{{template "txdetail" .}} {{end}}
</div>
{{template "txdetail" .}}
</div>
{{end}}

View File

@ -1,11 +1,11 @@
{{define "txdetail"}}{{$cs := .CoinShortcut}}
{{define "txdetail"}}{{$cs := .CoinShortcut}}{{$addr := .AddrStr}}{{$tx := .Tx}}
<div class="alert alert-data">
<div class="row line-bot">
<div class="col-xs-7 col-md-8 ellipsis">
<a href="/explorer/tx/{{.Txid}}">{{.Txid}}</a>
<a href="/explorer/tx/{{$tx.Txid}}">{{$tx.Txid}}</a>
</div>
{{if .Confirmations}}
<div class="col-xs-5 col-md-4 text-muted text-right">mined {{formatUnixTime .Blocktime}}</div>
{{if $tx.Confirmations}}
<div class="col-xs-5 col-md-4 text-muted text-right">mined {{formatUnixTime $tx.Blocktime}}</div>
{{end}}
</div>
<div class="row line-mid">
@ -13,7 +13,7 @@
<div class="row">
<table class="table data-table">
<tbody>
{{range $vin := .Vin}}
{{range $vin := $tx.Vin}}
<tr>
<td>
{{if $vin.Txid}}
@ -40,7 +40,7 @@
<div class="row">
<table class="table data-table">
<tbody>
{{range $vout := .Vout}}
{{range $vout := $tx.Vout}}
<tr>
<td>
{{range $a := $vout.ScriptPubKey.Addresses}}
@ -61,17 +61,17 @@
</div>
<div class="row line-top">
<div class="col-xs-6 col-sm-4 col-md-4">
{{if .Fees}}
<span class="txvalues txvalues-default">Fee: {{formatAmount .Fees}} {{$cs}}</span>
{{if $tx.Fees}}
<span class="txvalues txvalues-default">Fee: {{formatAmount $tx.Fees}} {{$cs}}</span>
{{end}}
</div>
<div class="col-xs-6 col-sm-8 col-md-8 text-right">
{{if .Confirmations}}
<span class="txvalues txvalues-success">{{.Confirmations}} Confirmations</span>
{{if $tx.Confirmations}}
<span class="txvalues txvalues-success">{{$tx.Confirmations}} Confirmations</span>
{{else}}
<span class="txvalues txvalues-danger ng-hide">Unconfirmed Transaction!</span>
{{end}}
<span class="txvalues txvalues-primary">{{formatAmount .ValueOut}} {{$cs}}</span>
<span class="txvalues txvalues-primary">{{formatAmount $tx.ValueOut}} {{$cs}}</span>
</div>
</div>
</div>