Unify handling and error handling of pages in public interface

pull/56/head
Martin Boehm 2018-08-24 16:17:43 +02:00
parent ad5ddbd029
commit 7d708ef868
5 changed files with 220 additions and 117 deletions

View File

@ -2,6 +2,22 @@ package api
import "math/big"
type ApiError struct {
Text string
Public bool
}
func (e *ApiError) Error() string {
return e.Text
}
func NewApiError(s string, public bool) error {
return &ApiError{
Text: s,
Public: public,
}
}
type ScriptSig struct {
Hex string `json:"hex"`
Asm string `json:"asm,omitempty"`

View File

@ -4,10 +4,11 @@ import (
"blockbook/bchain"
"blockbook/common"
"blockbook/db"
"errors"
"math/big"
"time"
"github.com/golang/glog"
"github.com/juju/errors"
)
// Worker is handle to api worker
@ -35,13 +36,13 @@ func NewWorker(db *db.RocksDB, chain bchain.BlockChain, txCache *db.TxCache, is
func (w *Worker) GetTransaction(txid string, bestheight uint32, spendingTx bool) (*Tx, error) {
bchainTx, height, err := w.txCache.GetTransaction(txid, bestheight)
if err != nil {
return nil, err
return nil, errors.Annotatef(err, "txCache.GetTransaction %v", txid)
}
var blockhash string
if bchainTx.Confirmations > 0 {
blockhash, err = w.db.GetBlockHash(height)
if err != nil {
return nil, err
return nil, errors.Annotatef(err, "GetBlockHash %v", height)
}
}
var valInSat, valOutSat, feesSat big.Int
@ -57,7 +58,7 @@ func (w *Worker) GetTransaction(txid string, bestheight uint32, spendingTx bool)
if bchainVin.Txid != "" {
otx, _, err := w.txCache.GetTransaction(bchainVin.Txid, bestheight)
if err != nil {
return nil, err
return nil, errors.Annotatef(err, "txCache.GetTransaction %v", bchainVin.Txid)
}
if len(otx.Vout) > int(vin.Vout) {
vout := &otx.Vout[vin.Vout]
@ -220,28 +221,28 @@ func (w *Worker) txFromTxAddress(txid string, ta *db.TxAddresses, bi *db.BlockIn
}
// GetAddress computes address value and gets transactions for given address
func (w *Worker) GetAddress(address string, page int, txsOnPage int) (*Address, error) {
glog.Info(address, " start")
func (w *Worker) GetAddress(address string, page int, txsOnPage int, onlyTxids bool) (*Address, error) {
start := time.Now()
ba, err := w.db.GetAddressBalance(address)
if err != nil {
return nil, err
return nil, errors.Annotatef(err, "GetAddressBalance %v", address)
}
if ba == nil {
return nil, errors.New("Address not found")
return nil, NewApiError("Address not found", true)
}
txc, err := w.getAddressTxids(address, false)
txc = UniqueTxidsInReverse(txc)
if err != nil {
return nil, err
return nil, errors.Annotatef(err, "getAddressTxids %v false", address)
}
txc = UniqueTxidsInReverse(txc)
txm, err := w.getAddressTxids(address, true)
if err != nil {
return nil, err
return nil, errors.Annotatef(err, "getAddressTxids %v true", address)
}
txm = UniqueTxidsInReverse(txm)
bestheight, _, err := w.db.GetBestBlock()
if err != nil {
return nil, err
return nil, errors.Annotatef(err, "GetBestBlock")
}
// paging
if page < 0 {
@ -268,7 +269,7 @@ func (w *Worker) GetAddress(address string, page int, txsOnPage int) (*Address,
tx, err := w.GetTransaction(tx, bestheight, false)
// mempool transaction may fail
if err != nil {
glog.Error("GetTransaction ", tx, ": ", err)
glog.Error("GetTransaction in mempool ", tx, ": ", err)
} else {
uBalSat.Sub(tx.getAddrVoutValue(address), tx.getAddrVinValue(address))
txs[txi] = tx
@ -282,7 +283,7 @@ func (w *Worker) GetAddress(address string, page int, txsOnPage int) (*Address,
txid := txc[i]
ta, err := w.db.GetTxAddresses(txid)
if err != nil {
return nil, err
return nil, errors.Annotatef(err, "GetTxAddresses %v", txid)
}
if ta == nil {
glog.Warning("DB inconsistency: tx ", txid, ": not found in txAddresses")
@ -290,7 +291,7 @@ func (w *Worker) GetAddress(address string, page int, txsOnPage int) (*Address,
}
bi, err := w.db.GetBlockInfo(ta.Height)
if err != nil {
return nil, err
return nil, errors.Annotatef(err, "GetBlockInfo %v", ta.Height)
}
if bi == nil {
glog.Warning("DB inconsistency: block height ", ta.Height, ": not found in db")
@ -312,7 +313,7 @@ func (w *Worker) GetAddress(address string, page int, txsOnPage int) (*Address,
TotalPages: totalPages,
TxsOnPage: txsOnPage,
}
glog.Info(address, " finished")
glog.Info(address, " finished in ", time.Since(start))
return r, nil
}

View File

@ -55,6 +55,8 @@ var (
syncWorkers = flag.Int("workers", 8, "number of workers to process blocks")
dryRun = flag.Bool("dryrun", false, "do not index blocks, only download")
debugMode = flag.Bool("debug", false, "debug mode, return more verbose errors, reload templates on each request")
internalBinding = flag.String("internal", "", "internal http server binding [address]:port, (default no internal server)")
publicBinding = flag.String("public", "", "public http server binding [address]:port[/path], (default no public server)")
@ -130,7 +132,7 @@ func main() {
chanOsSignal = make(chan os.Signal, 1)
signal.Notify(chanOsSignal, syscall.SIGHUP, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGTERM)
glog.Infof("Blockbook: %+v", common.GetVersionInfo())
glog.Infof("Blockbook: %+v, debug mode %v", common.GetVersionInfo(), *debugMode)
if *prof != "" {
go func() {
@ -270,7 +272,7 @@ func main() {
var publicServer *server.PublicServer
if *publicBinding != "" {
publicServer, err = server.NewPublicServer(*publicBinding, *certFiles, index, chain, txCache, *explorerURL, metrics, internalState)
publicServer, err = server.NewPublicServer(*publicBinding, *certFiles, index, chain, txCache, *explorerURL, metrics, internalState, *debugMode)
if err != nil {
glog.Error("socketio: ", err)
return

View File

@ -10,6 +10,8 @@ import (
"fmt"
"html/template"
"net/http"
"reflect"
"runtime"
"strconv"
"strings"
"time"
@ -35,12 +37,12 @@ type PublicServer struct {
explorerURL string
metrics *common.Metrics
is *common.InternalState
txTpl *template.Template
addressTpl *template.Template
templates []*template.Template
debug bool
}
// NewPublicServer creates new public server http interface to blockbook and returns its handle
func NewPublicServer(binding string, certFiles string, db *db.RocksDB, chain bchain.BlockChain, txCache *db.TxCache, explorerURL string, metrics *common.Metrics, is *common.InternalState) (*PublicServer, error) {
func NewPublicServer(binding string, certFiles string, db *db.RocksDB, chain bchain.BlockChain, txCache *db.TxCache, explorerURL string, metrics *common.Metrics, is *common.InternalState, debugMode bool) (*PublicServer, error) {
api, err := api.NewWorker(db, chain, txCache, is)
if err != nil {
@ -72,55 +74,34 @@ func NewPublicServer(binding string, certFiles string, db *db.RocksDB, chain bch
explorerURL: explorerURL,
metrics: metrics,
is: is,
debug: debugMode,
}
// favicon
serveMux.Handle(path+"favicon.ico", http.FileServer(http.Dir("./static/")))
// support for tests of socket.io interface
serveMux.Handle(path+"test.html", http.FileServer(http.Dir("./static/")))
// redirect to Bitcore for details of transaction
// redirect to wallet requests for tx and address, possibly to external site
serveMux.HandleFunc(path+"tx/", s.txRedirect)
serveMux.HandleFunc(path+"address/", s.addressRedirect)
// explorer
serveMux.HandleFunc(path+"explorer/tx/", s.explorerTx)
serveMux.HandleFunc(path+"explorer/address/", s.explorerAddress)
serveMux.HandleFunc(path+"explorer/tx/", s.htmlTemplateHandler(s.explorerTx))
serveMux.HandleFunc(path+"explorer/address/", s.htmlTemplateHandler(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)
serveMux.HandleFunc(path+"api/block-index/", s.jsonHandler(s.apiBlockIndex))
serveMux.HandleFunc(path+"api/tx/", s.jsonHandler(s.apiTx))
serveMux.HandleFunc(path+"api/address/", s.jsonHandler(s.apiAddress))
// handle socket.io
serveMux.Handle(path+"socket.io/", socketio.GetHandler())
// default handler
serveMux.HandleFunc(path, s.index)
s.txTpl, s.addressTpl = parseTemplates()
s.templates = parseTemplates()
return s, nil
}
func parseTemplates() (txTpl, addressTpl *template.Template) {
templateFuncMap := template.FuncMap{
"formatUnixTime": formatUnixTime,
"formatAmount": formatAmount,
"setTxToTemplateData": setTxToTemplateData,
"stringInSlice": stringInSlice,
}
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
}
func formatUnixTime(ut int64) string {
return time.Unix(ut, 0).Format(time.RFC1123)
}
// for now return the string as it is
// in future could be used to do coin specific formatting
func formatAmount(a string) string {
return a
}
// Run starts the server
func (s *PublicServer) Run() error {
if s.certFiles == "" {
@ -153,6 +134,20 @@ func (s *PublicServer) OnNewTxAddr(txid string, addr string) {
s.socketio.OnNewTxAddr(txid, addr)
}
func (s *PublicServer) txRedirect(w http.ResponseWriter, r *http.Request) {
if s.explorerURL != "" {
http.Redirect(w, r, joinURL(s.explorerURL, r.URL.Path), 302)
s.metrics.ExplorerViews.With(common.Labels{"action": "tx"}).Inc()
}
}
func (s *PublicServer) addressRedirect(w http.ResponseWriter, r *http.Request) {
if s.explorerURL != "" {
http.Redirect(w, r, joinURL(s.explorerURL, r.URL.Path), 302)
s.metrics.ExplorerViews.With(common.Labels{"action": "address"}).Inc()
}
}
func splitBinding(binding string) (addr string, path string) {
i := strings.Index(binding, "/")
if i >= 0 {
@ -171,34 +166,154 @@ func joinURL(base string, part string) string {
return part
}
func (s *PublicServer) txRedirect(w http.ResponseWriter, r *http.Request) {
if s.explorerURL != "" {
http.Redirect(w, r, joinURL(s.explorerURL, r.URL.Path), 302)
s.metrics.ExplorerViews.With(common.Labels{"action": "tx"}).Inc()
func getFunctionName(i interface{}) string {
return runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name()
}
func (s *PublicServer) jsonHandler(handler func(r *http.Request) (interface{}, error)) func(w http.ResponseWriter, r *http.Request) {
type jsonError struct {
Error string `json:"error"`
}
return func(w http.ResponseWriter, r *http.Request) {
var data interface{}
var err error
defer func() {
if e := recover(); e != nil {
glog.Error(getFunctionName(handler), " recovered from panic: ", e)
if s.debug {
data = jsonError{fmt.Sprint("Internal server error: recovered from panic ", e)}
} else {
data = jsonError{"Internal server error"}
}
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
json.NewEncoder(w).Encode(data)
}()
data, err = handler(r)
if err != nil || data == nil {
if apiErr, ok := err.(*api.ApiError); ok {
data = jsonError{apiErr.Error()}
} else {
if err != nil {
glog.Error(getFunctionName(handler), " error: ", err)
}
if s.debug {
data = jsonError{fmt.Sprintf("Internal server error: %v, data %+v", err, data)}
} else {
data = jsonError{"Internal server error"}
}
}
}
}
}
func (s *PublicServer) addressRedirect(w http.ResponseWriter, r *http.Request) {
if s.explorerURL != "" {
http.Redirect(w, r, joinURL(s.explorerURL, r.URL.Path), 302)
s.metrics.ExplorerViews.With(common.Labels{"action": "address"}).Inc()
func (s *PublicServer) newTemplateData() *TemplateData {
return &TemplateData{
CoinName: s.is.Coin,
CoinShortcut: s.is.CoinShortcut,
}
}
func (s *PublicServer) newTemplateDataWithError(text string) *TemplateData {
return &TemplateData{
CoinName: s.is.Coin,
CoinShortcut: s.is.CoinShortcut,
Error: &api.ApiError{Text: text},
}
}
func (s *PublicServer) htmlTemplateHandler(handler func(r *http.Request) (tpl, *TemplateData, error)) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
var t tpl
var data *TemplateData
var err error
defer func() {
if e := recover(); e != nil {
glog.Error(getFunctionName(handler), " recovered from panic: ", e)
t = errorTpl
if s.debug {
data = s.newTemplateDataWithError(fmt.Sprint("Internal server error: recovered from panic ", e))
} else {
data = s.newTemplateDataWithError("Internal server error")
}
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
if err := s.templates[t].ExecuteTemplate(w, "base.html", data); err != nil {
glog.Error(err)
}
}()
if s.debug {
// reload templates on each request
// to reflect changes during development
s.templates = parseTemplates()
}
t, data, err = handler(r)
if err != nil || data == nil {
t = errorTpl
if apiErr, ok := err.(*api.ApiError); ok {
data = s.newTemplateData()
data.Error = apiErr
} else {
if err != nil {
glog.Error(getFunctionName(handler), " error: ", err)
}
if s.debug {
data = s.newTemplateDataWithError(fmt.Sprintf("Internal server error: %v, data %+v", err, data))
} else {
data = s.newTemplateDataWithError("Internal server error")
}
}
}
}
}
type tpl int
const (
errorTpl = tpl(iota)
txTpl
addressTpl
)
type TemplateData struct {
CoinName string
CoinShortcut string
Address *api.Address
AddrStr string
Tx *api.Tx
Error *api.ApiError
}
func parseTemplates() []*template.Template {
templateFuncMap := template.FuncMap{
"formatUnixTime": formatUnixTime,
"formatAmount": formatAmount,
"setTxToTemplateData": setTxToTemplateData,
"stringInSlice": stringInSlice,
}
t := make([]*template.Template, 3)
t[errorTpl] = template.Must(template.New("tx").Funcs(templateFuncMap).ParseFiles("./static/templates/error.html", "./static/templates/base.html"))
t[txTpl] = template.Must(template.New("tx").Funcs(templateFuncMap).ParseFiles("./static/templates/tx.html", "./static/templates/txdetail.html", "./static/templates/base.html"))
t[addressTpl] = template.Must(template.New("address").Funcs(templateFuncMap).ParseFiles("./static/templates/address.html", "./static/templates/txdetail.html", "./static/templates/base.html"))
return t
}
func formatUnixTime(ut int64) string {
return time.Unix(ut, 0).Format(time.RFC1123)
}
// for now return the string as it is
// in future could be used to do coin specific formatting
func formatAmount(a string) string {
return a
}
// called from template to support txdetail.html functionality
func setTxToTemplateData(td *TemplateData, tx *api.Tx) *TemplateData {
td.Tx = tx
return td
}
func (s *PublicServer) explorerTx(w http.ResponseWriter, r *http.Request) {
func (s *PublicServer) explorerTx(r *http.Request) (tpl, *TemplateData, error) {
var tx *api.Tx
if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 {
txid := r.URL.Path[i+1:]
@ -207,26 +322,15 @@ func (s *PublicServer) explorerTx(w http.ResponseWriter, r *http.Request) {
tx, err = s.api.GetTransaction(txid, bestheight, true)
}
if err != nil {
glog.Error(err)
return errorTpl, nil, 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,
Tx: tx,
}
if err := s.txTpl.ExecuteTemplate(w, "base.html", data); err != nil {
glog.Error(err)
}
data := s.newTemplateData()
data.Tx = tx
return txTpl, data, nil
}
func (s *PublicServer) explorerAddress(w http.ResponseWriter, r *http.Request) {
func (s *PublicServer) explorerAddress(r *http.Request) (tpl, *TemplateData, error) {
var address *api.Address
var err error
if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 {
@ -235,27 +339,15 @@ func (s *PublicServer) explorerAddress(w http.ResponseWriter, r *http.Request) {
page = 0
}
addrID := r.URL.Path[i+1:]
address, err = s.api.GetAddress(addrID, page, txsOnPage)
address, err = s.api.GetAddress(addrID, page, txsOnPage, false)
if err != nil {
glog.Error(err)
// TODO return error.html
return errorTpl, nil, 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)
}
data := s.newTemplateData()
data.AddrStr = address.AddrStr
data.Address = address
return addressTpl, data, nil
}
type resAboutBlockbookPublic struct {
@ -272,6 +364,7 @@ type resAboutBlockbookPublic struct {
About string `json:"about"`
}
// TODO - this is temporary, return html status page
func (s *PublicServer) index(w http.ResponseWriter, r *http.Request) {
vi := common.GetVersionInfo()
ss, bh, st := s.is.GetSyncState()
@ -297,7 +390,7 @@ func (s *PublicServer) index(w http.ResponseWriter, r *http.Request) {
w.Write(buf)
}
func (s *PublicServer) apiBlockIndex(w http.ResponseWriter, r *http.Request) {
func (s *PublicServer) apiBlockIndex(r *http.Request) (interface{}, error) {
type resBlockIndex struct {
BlockHash string `json:"blockHash"`
About string `json:"about"`
@ -317,17 +410,15 @@ func (s *PublicServer) apiBlockIndex(w http.ResponseWriter, r *http.Request) {
}
if err != nil {
glog.Error(err)
} else {
r := resBlockIndex{
BlockHash: hash,
About: blockbookAbout,
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
json.NewEncoder(w).Encode(r)
return nil, err
}
return resBlockIndex{
BlockHash: hash,
About: blockbookAbout,
}, nil
}
func (s *PublicServer) apiTx(w http.ResponseWriter, r *http.Request) {
func (s *PublicServer) apiTx(r *http.Request) (interface{}, error) {
var tx *api.Tx
var err error
if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 {
@ -335,17 +426,12 @@ func (s *PublicServer) apiTx(w http.ResponseWriter, r *http.Request) {
bestheight, _, err := s.db.GetBestBlock()
if err == nil {
tx, err = s.api.GetTransaction(txid, bestheight, true)
} else {
glog.Error(err)
}
}
if err == nil {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
json.NewEncoder(w).Encode(tx)
}
return tx, err
}
func (s *PublicServer) apiAddress(w http.ResponseWriter, r *http.Request) {
func (s *PublicServer) apiAddress(r *http.Request) (interface{}, error) {
var address *api.Address
var err error
if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 {
@ -354,13 +440,7 @@ func (s *PublicServer) apiAddress(w http.ResponseWriter, r *http.Request) {
page = 0
}
addrID := r.URL.Path[i+1:]
address, err = s.api.GetAddress(addrID, page, txsInAPI)
if err != nil {
glog.Error(err)
}
}
if err == nil {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
json.NewEncoder(w).Encode(address)
address, err = s.api.GetAddress(addrID, page, txsInAPI, true)
}
return address, err
}

View File

@ -0,0 +1,4 @@
{{define "specific"}}
<h1>Error</h1>
<h3>{{.Error.Text}}</h3>
{{end}}