Add initial implementation of address explorer
This commit is contained in:
parent
20643756e5
commit
7e68630377
16
api/types.go
16
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"`
|
||||
}
|
||||
|
||||
104
api/worker.go
104
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
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
52
static/templates/address.html
Normal file
52
static/templates/address.html
Normal 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}}
|
||||
@ -48,7 +48,7 @@
|
||||
</header>
|
||||
<main id="wrap">
|
||||
<div class="container">
|
||||
{{template "specific" .Specific}}
|
||||
{{template "specific" .}}
|
||||
</div>
|
||||
</main>
|
||||
<footer id="footer" class="footer">
|
||||
|
||||
@ -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}}
|
||||
@ -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>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user