github.com/aychain/blockbook@v0.1.1-0.20181121092459-6d1fc7e07c5b/server/public.go (about)

     1  package server
     2  
     3  import (
     4  	"blockbook/api"
     5  	"blockbook/bchain"
     6  	"blockbook/common"
     7  	"blockbook/db"
     8  	"context"
     9  	"encoding/json"
    10  	"fmt"
    11  	"html/template"
    12  	"io/ioutil"
    13  	"math/big"
    14  	"net/http"
    15  	"reflect"
    16  	"runtime"
    17  	"strconv"
    18  	"strings"
    19  	"time"
    20  
    21  	"github.com/golang/glog"
    22  )
    23  
    24  const txsOnPage = 25
    25  const blocksOnPage = 50
    26  const txsInAPI = 1000
    27  
    28  // PublicServer is a handle to public http server
    29  type PublicServer struct {
    30  	binding          string
    31  	certFiles        string
    32  	socketio         *SocketIoServer
    33  	https            *http.Server
    34  	db               *db.RocksDB
    35  	txCache          *db.TxCache
    36  	chain            bchain.BlockChain
    37  	chainParser      bchain.BlockChainParser
    38  	api              *api.Worker
    39  	explorerURL      string
    40  	internalExplorer bool
    41  	metrics          *common.Metrics
    42  	is               *common.InternalState
    43  	templates        []*template.Template
    44  	debug            bool
    45  }
    46  
    47  // NewPublicServer creates new public server http interface to blockbook and returns its handle
    48  // only basic functionality is mapped, to map all functions, call
    49  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) {
    50  
    51  	api, err := api.NewWorker(db, chain, txCache, is)
    52  	if err != nil {
    53  		return nil, err
    54  	}
    55  
    56  	socketio, err := NewSocketIoServer(db, chain, txCache, metrics, is)
    57  	if err != nil {
    58  		return nil, err
    59  	}
    60  
    61  	addr, path := splitBinding(binding)
    62  	serveMux := http.NewServeMux()
    63  	https := &http.Server{
    64  		Addr:    addr,
    65  		Handler: serveMux,
    66  	}
    67  
    68  	s := &PublicServer{
    69  		binding:          binding,
    70  		certFiles:        certFiles,
    71  		https:            https,
    72  		api:              api,
    73  		socketio:         socketio,
    74  		db:               db,
    75  		txCache:          txCache,
    76  		chain:            chain,
    77  		chainParser:      chain.GetChainParser(),
    78  		explorerURL:      explorerURL,
    79  		internalExplorer: explorerURL == "",
    80  		metrics:          metrics,
    81  		is:               is,
    82  		debug:            debugMode,
    83  	}
    84  	s.templates = parseTemplates()
    85  
    86  	// map only basic functions, the rest is enabled by method MapFullPublicInterface
    87  	serveMux.Handle(path+"favicon.ico", http.FileServer(http.Dir("./static/")))
    88  	serveMux.Handle(path+"static/", http.StripPrefix("/static/", http.FileServer(http.Dir("./static/"))))
    89  	// default handler
    90  	serveMux.HandleFunc(path, s.htmlTemplateHandler(s.explorerIndex))
    91  	// default API handler
    92  	serveMux.HandleFunc(path+"api/", s.jsonHandler(s.apiIndex))
    93  
    94  	return s, nil
    95  }
    96  
    97  // Run starts the server
    98  func (s *PublicServer) Run() error {
    99  	if s.certFiles == "" {
   100  		glog.Info("public server: starting to listen on http://", s.https.Addr)
   101  		return s.https.ListenAndServe()
   102  	}
   103  	glog.Info("public server starting to listen on https://", s.https.Addr)
   104  	return s.https.ListenAndServeTLS(fmt.Sprint(s.certFiles, ".crt"), fmt.Sprint(s.certFiles, ".key"))
   105  }
   106  
   107  // ConnectFullPublicInterface enables complete public functionality
   108  func (s *PublicServer) ConnectFullPublicInterface() {
   109  	serveMux := s.https.Handler.(*http.ServeMux)
   110  	_, path := splitBinding(s.binding)
   111  	// support for tests of socket.io interface
   112  	serveMux.Handle(path+"test.html", http.FileServer(http.Dir("./static/")))
   113  	if s.internalExplorer {
   114  		// internal explorer handlers
   115  		serveMux.HandleFunc(path+"tx/", s.htmlTemplateHandler(s.explorerTx))
   116  		serveMux.HandleFunc(path+"address/", s.htmlTemplateHandler(s.explorerAddress))
   117  		serveMux.HandleFunc(path+"search/", s.htmlTemplateHandler(s.explorerSearch))
   118  		serveMux.HandleFunc(path+"blocks", s.htmlTemplateHandler(s.explorerBlocks))
   119  		serveMux.HandleFunc(path+"block/", s.htmlTemplateHandler(s.explorerBlock))
   120  		serveMux.HandleFunc(path+"spending/", s.htmlTemplateHandler(s.explorerSpendingTx))
   121  		serveMux.HandleFunc(path+"sendtx", s.htmlTemplateHandler(s.explorerSendTx))
   122  	} else {
   123  		// redirect to wallet requests for tx and address, possibly to external site
   124  		serveMux.HandleFunc(path+"tx/", s.txRedirect)
   125  		serveMux.HandleFunc(path+"address/", s.addressRedirect)
   126  	}
   127  	// API calls
   128  	serveMux.HandleFunc(path+"api/block-index/", s.jsonHandler(s.apiBlockIndex))
   129  	serveMux.HandleFunc(path+"api/tx/", s.jsonHandler(s.apiTx))
   130  	serveMux.HandleFunc(path+"api/tx-specific/", s.jsonHandler(s.apiTxSpecific))
   131  	serveMux.HandleFunc(path+"api/address/", s.jsonHandler(s.apiAddress))
   132  	serveMux.HandleFunc(path+"api/utxo/", s.jsonHandler(s.apiAddressUtxo))
   133  	serveMux.HandleFunc(path+"api/block/", s.jsonHandler(s.apiBlock))
   134  	serveMux.HandleFunc(path+"api/sendtx/", s.jsonHandler(s.apiSendTx))
   135  	serveMux.HandleFunc(path+"api/estimatefee/", s.jsonHandler(s.apiEstimateFee))
   136  	// socket.io interface
   137  	serveMux.Handle(path+"socket.io/", s.socketio.GetHandler())
   138  }
   139  
   140  // Close closes the server
   141  func (s *PublicServer) Close() error {
   142  	glog.Infof("public server: closing")
   143  	return s.https.Close()
   144  }
   145  
   146  // Shutdown shuts down the server
   147  func (s *PublicServer) Shutdown(ctx context.Context) error {
   148  	glog.Infof("public server: shutdown")
   149  	return s.https.Shutdown(ctx)
   150  }
   151  
   152  // OnNewBlock notifies users subscribed to bitcoind/hashblock about new block
   153  func (s *PublicServer) OnNewBlock(hash string, height uint32) {
   154  	s.socketio.OnNewBlockHash(hash)
   155  }
   156  
   157  // OnNewTxAddr notifies users subscribed to bitcoind/addresstxid about new block
   158  func (s *PublicServer) OnNewTxAddr(txid string, desc bchain.AddressDescriptor, isOutput bool) {
   159  	s.socketio.OnNewTxAddr(txid, desc, isOutput)
   160  }
   161  
   162  func (s *PublicServer) txRedirect(w http.ResponseWriter, r *http.Request) {
   163  	http.Redirect(w, r, joinURL(s.explorerURL, r.URL.Path), 302)
   164  	s.metrics.ExplorerViews.With(common.Labels{"action": "tx-redirect"}).Inc()
   165  }
   166  
   167  func (s *PublicServer) addressRedirect(w http.ResponseWriter, r *http.Request) {
   168  	http.Redirect(w, r, joinURL(s.explorerURL, r.URL.Path), 302)
   169  	s.metrics.ExplorerViews.With(common.Labels{"action": "address-redirect"}).Inc()
   170  }
   171  
   172  func splitBinding(binding string) (addr string, path string) {
   173  	i := strings.Index(binding, "/")
   174  	if i >= 0 {
   175  		return binding[0:i], binding[i:]
   176  	}
   177  	return binding, "/"
   178  }
   179  
   180  func joinURL(base string, part string) string {
   181  	if len(base) > 0 {
   182  		if len(base) > 0 && base[len(base)-1] == '/' && len(part) > 0 && part[0] == '/' {
   183  			return base + part[1:]
   184  		}
   185  		return base + part
   186  	}
   187  	return part
   188  }
   189  
   190  func getFunctionName(i interface{}) string {
   191  	return runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name()
   192  }
   193  
   194  func (s *PublicServer) jsonHandler(handler func(r *http.Request) (interface{}, error)) func(w http.ResponseWriter, r *http.Request) {
   195  	type jsonError struct {
   196  		Text       string `json:"error"`
   197  		HTTPStatus int    `json:"-"`
   198  	}
   199  	return func(w http.ResponseWriter, r *http.Request) {
   200  		var data interface{}
   201  		var err error
   202  		defer func() {
   203  			if e := recover(); e != nil {
   204  				glog.Error(getFunctionName(handler), " recovered from panic: ", e)
   205  				if s.debug {
   206  					data = jsonError{fmt.Sprint("Internal server error: recovered from panic ", e), http.StatusInternalServerError}
   207  				} else {
   208  					data = jsonError{"Internal server error", http.StatusInternalServerError}
   209  				}
   210  			}
   211  			w.Header().Set("Content-Type", "application/json; charset=utf-8")
   212  			if e, isError := data.(jsonError); isError {
   213  				w.WriteHeader(e.HTTPStatus)
   214  			}
   215  			json.NewEncoder(w).Encode(data)
   216  		}()
   217  		data, err = handler(r)
   218  		if err != nil || data == nil {
   219  			if apiErr, ok := err.(*api.APIError); ok {
   220  				if apiErr.Public {
   221  					data = jsonError{apiErr.Error(), http.StatusBadRequest}
   222  				} else {
   223  					data = jsonError{apiErr.Error(), http.StatusInternalServerError}
   224  				}
   225  			} else {
   226  				if err != nil {
   227  					glog.Error(getFunctionName(handler), " error: ", err)
   228  				}
   229  				if s.debug {
   230  					if data != nil {
   231  						data = jsonError{fmt.Sprintf("Internal server error: %v, data %+v", err, data), http.StatusInternalServerError}
   232  					} else {
   233  						data = jsonError{fmt.Sprintf("Internal server error: %v", err), http.StatusInternalServerError}
   234  					}
   235  				} else {
   236  					data = jsonError{"Internal server error", http.StatusInternalServerError}
   237  				}
   238  			}
   239  		}
   240  	}
   241  }
   242  
   243  func (s *PublicServer) newTemplateData() *TemplateData {
   244  	return &TemplateData{
   245  		CoinName:         s.is.Coin,
   246  		CoinShortcut:     s.is.CoinShortcut,
   247  		CoinLabel:        s.is.CoinLabel,
   248  		InternalExplorer: s.internalExplorer && !s.is.InitialSync,
   249  		TOSLink:          api.Text.TOSLink,
   250  	}
   251  }
   252  
   253  func (s *PublicServer) newTemplateDataWithError(text string) *TemplateData {
   254  	td := s.newTemplateData()
   255  	td.Error = &api.APIError{Text: text}
   256  	return td
   257  }
   258  
   259  func (s *PublicServer) htmlTemplateHandler(handler func(w http.ResponseWriter, r *http.Request) (tpl, *TemplateData, error)) func(w http.ResponseWriter, r *http.Request) {
   260  	return func(w http.ResponseWriter, r *http.Request) {
   261  		var t tpl
   262  		var data *TemplateData
   263  		var err error
   264  		defer func() {
   265  			if e := recover(); e != nil {
   266  				glog.Error(getFunctionName(handler), " recovered from panic: ", e)
   267  				t = errorInternalTpl
   268  				if s.debug {
   269  					data = s.newTemplateDataWithError(fmt.Sprint("Internal server error: recovered from panic ", e))
   270  				} else {
   271  					data = s.newTemplateDataWithError("Internal server error")
   272  				}
   273  			}
   274  			// noTpl means the handler completely handled the request
   275  			if t != noTpl {
   276  				w.Header().Set("Content-Type", "text/html; charset=utf-8")
   277  				// return 500 Internal Server Error with errorInternalTpl
   278  				if t == errorInternalTpl {
   279  					w.WriteHeader(http.StatusInternalServerError)
   280  				}
   281  				if err := s.templates[t].ExecuteTemplate(w, "base.html", data); err != nil {
   282  					glog.Error(err)
   283  				}
   284  			}
   285  		}()
   286  		if s.debug {
   287  			// reload templates on each request
   288  			// to reflect changes during development
   289  			s.templates = parseTemplates()
   290  		}
   291  		t, data, err = handler(w, r)
   292  		if err != nil || (data == nil && t != noTpl) {
   293  			t = errorInternalTpl
   294  			if apiErr, ok := err.(*api.APIError); ok {
   295  				data = s.newTemplateData()
   296  				data.Error = apiErr
   297  				if apiErr.Public {
   298  					t = errorTpl
   299  				}
   300  			} else {
   301  				if err != nil {
   302  					glog.Error(getFunctionName(handler), " error: ", err)
   303  				}
   304  				if s.debug {
   305  					data = s.newTemplateDataWithError(fmt.Sprintf("Internal server error: %v, data %+v", err, data))
   306  				} else {
   307  					data = s.newTemplateDataWithError("Internal server error")
   308  				}
   309  			}
   310  		}
   311  	}
   312  }
   313  
   314  type tpl int
   315  
   316  const (
   317  	noTpl = tpl(iota)
   318  	errorTpl
   319  	errorInternalTpl
   320  	indexTpl
   321  	txTpl
   322  	addressTpl
   323  	blocksTpl
   324  	blockTpl
   325  	sendTransactionTpl
   326  
   327  	tplCount
   328  )
   329  
   330  // TemplateData is used to transfer data to the templates
   331  type TemplateData struct {
   332  	CoinName         string
   333  	CoinShortcut     string
   334  	CoinLabel        string
   335  	InternalExplorer bool
   336  	Address          *api.Address
   337  	AddrStr          string
   338  	Tx               *api.Tx
   339  	TxSpecific       json.RawMessage
   340  	Error            *api.APIError
   341  	Blocks           *api.Blocks
   342  	Block            *api.Block
   343  	Info             *api.SystemInfo
   344  	Page             int
   345  	PrevPage         int
   346  	NextPage         int
   347  	PagingRange      []int
   348  	TOSLink          string
   349  	SendTxHex        string
   350  	Status           string
   351  }
   352  
   353  func parseTemplates() []*template.Template {
   354  	templateFuncMap := template.FuncMap{
   355  		"formatTime":          formatTime,
   356  		"formatUnixTime":      formatUnixTime,
   357  		"formatAmount":        formatAmount,
   358  		"setTxToTemplateData": setTxToTemplateData,
   359  		"stringInSlice":       stringInSlice,
   360  	}
   361  	t := make([]*template.Template, tplCount)
   362  	t[errorTpl] = template.Must(template.New("error").Funcs(templateFuncMap).ParseFiles("./static/templates/error.html", "./static/templates/base.html"))
   363  	t[errorInternalTpl] = template.Must(template.New("error").Funcs(templateFuncMap).ParseFiles("./static/templates/error.html", "./static/templates/base.html"))
   364  	t[indexTpl] = template.Must(template.New("index").Funcs(templateFuncMap).ParseFiles("./static/templates/index.html", "./static/templates/base.html"))
   365  	t[txTpl] = template.Must(template.New("tx").Funcs(templateFuncMap).ParseFiles("./static/templates/tx.html", "./static/templates/txdetail.html", "./static/templates/base.html"))
   366  	t[addressTpl] = template.Must(template.New("address").Funcs(templateFuncMap).ParseFiles("./static/templates/address.html", "./static/templates/txdetail.html", "./static/templates/paging.html", "./static/templates/base.html"))
   367  	t[blocksTpl] = template.Must(template.New("blocks").Funcs(templateFuncMap).ParseFiles("./static/templates/blocks.html", "./static/templates/paging.html", "./static/templates/base.html"))
   368  	t[blockTpl] = template.Must(template.New("block").Funcs(templateFuncMap).ParseFiles("./static/templates/block.html", "./static/templates/txdetail.html", "./static/templates/paging.html", "./static/templates/base.html"))
   369  	t[sendTransactionTpl] = template.Must(template.New("block").Funcs(templateFuncMap).ParseFiles("./static/templates/sendtx.html", "./static/templates/base.html"))
   370  	return t
   371  }
   372  
   373  func formatUnixTime(ut int64) string {
   374  	return formatTime(time.Unix(ut, 0))
   375  }
   376  
   377  func formatTime(t time.Time) string {
   378  	return t.Format(time.RFC1123)
   379  }
   380  
   381  // for now return the string as it is
   382  // in future could be used to do coin specific formatting
   383  func formatAmount(a string) string {
   384  	return a
   385  }
   386  
   387  // called from template to support txdetail.html functionality
   388  func setTxToTemplateData(td *TemplateData, tx *api.Tx) *TemplateData {
   389  	td.Tx = tx
   390  	return td
   391  }
   392  
   393  func (s *PublicServer) explorerTx(w http.ResponseWriter, r *http.Request) (tpl, *TemplateData, error) {
   394  	var tx *api.Tx
   395  	var txSpecific json.RawMessage
   396  	var err error
   397  	s.metrics.ExplorerViews.With(common.Labels{"action": "tx"}).Inc()
   398  	if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 {
   399  		txid := r.URL.Path[i+1:]
   400  		tx, err = s.api.GetTransaction(txid, false)
   401  		if err != nil {
   402  			return errorTpl, nil, err
   403  		}
   404  		txSpecific, err = s.chain.GetTransactionSpecific(txid)
   405  		if err != nil {
   406  			return errorTpl, nil, err
   407  		}
   408  	}
   409  	data := s.newTemplateData()
   410  	data.Tx = tx
   411  	data.TxSpecific = txSpecific
   412  	return txTpl, data, nil
   413  }
   414  
   415  func (s *PublicServer) explorerSpendingTx(w http.ResponseWriter, r *http.Request) (tpl, *TemplateData, error) {
   416  	s.metrics.ExplorerViews.With(common.Labels{"action": "spendingtx"}).Inc()
   417  	var err error
   418  	parts := strings.Split(r.URL.Path, "/")
   419  	if len(parts) > 2 {
   420  		tx := parts[len(parts)-2]
   421  		n, ec := strconv.Atoi(parts[len(parts)-1])
   422  		if ec == nil {
   423  			spendingTx, err := s.api.GetSpendingTxid(tx, n)
   424  			if err == nil && spendingTx != "" {
   425  				http.Redirect(w, r, joinURL("/tx/", spendingTx), 302)
   426  				return noTpl, nil, nil
   427  			}
   428  		}
   429  	}
   430  	if err == nil {
   431  		err = api.NewAPIError("Transaction not found", true)
   432  	}
   433  	return errorTpl, nil, err
   434  }
   435  
   436  func (s *PublicServer) explorerAddress(w http.ResponseWriter, r *http.Request) (tpl, *TemplateData, error) {
   437  	var address *api.Address
   438  	var err error
   439  	s.metrics.ExplorerViews.With(common.Labels{"action": "address"}).Inc()
   440  	if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 {
   441  		page, ec := strconv.Atoi(r.URL.Query().Get("page"))
   442  		if ec != nil {
   443  			page = 0
   444  		}
   445  		address, err = s.api.GetAddress(r.URL.Path[i+1:], page, txsOnPage, false)
   446  		if err != nil {
   447  			return errorTpl, nil, err
   448  		}
   449  	}
   450  	data := s.newTemplateData()
   451  	data.AddrStr = address.AddrStr
   452  	data.Address = address
   453  	data.Page = address.Page
   454  	data.PagingRange, data.PrevPage, data.NextPage = getPagingRange(address.Page, address.TotalPages)
   455  	return addressTpl, data, nil
   456  }
   457  
   458  func (s *PublicServer) explorerBlocks(w http.ResponseWriter, r *http.Request) (tpl, *TemplateData, error) {
   459  	var blocks *api.Blocks
   460  	var err error
   461  	s.metrics.ExplorerViews.With(common.Labels{"action": "blocks"}).Inc()
   462  	page, ec := strconv.Atoi(r.URL.Query().Get("page"))
   463  	if ec != nil {
   464  		page = 0
   465  	}
   466  	blocks, err = s.api.GetBlocks(page, blocksOnPage)
   467  	if err != nil {
   468  		return errorTpl, nil, err
   469  	}
   470  	data := s.newTemplateData()
   471  	data.Blocks = blocks
   472  	data.Page = blocks.Page
   473  	data.PagingRange, data.PrevPage, data.NextPage = getPagingRange(blocks.Page, blocks.TotalPages)
   474  	return blocksTpl, data, nil
   475  }
   476  
   477  func (s *PublicServer) explorerBlock(w http.ResponseWriter, r *http.Request) (tpl, *TemplateData, error) {
   478  	var block *api.Block
   479  	var err error
   480  	s.metrics.ExplorerViews.With(common.Labels{"action": "block"}).Inc()
   481  	if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 {
   482  		page, ec := strconv.Atoi(r.URL.Query().Get("page"))
   483  		if ec != nil {
   484  			page = 0
   485  		}
   486  		block, err = s.api.GetBlock(r.URL.Path[i+1:], page, txsOnPage)
   487  		if err != nil {
   488  			return errorTpl, nil, err
   489  		}
   490  	}
   491  	data := s.newTemplateData()
   492  	data.Block = block
   493  	data.Page = block.Page
   494  	data.PagingRange, data.PrevPage, data.NextPage = getPagingRange(block.Page, block.TotalPages)
   495  	return blockTpl, data, nil
   496  }
   497  
   498  func (s *PublicServer) explorerIndex(w http.ResponseWriter, r *http.Request) (tpl, *TemplateData, error) {
   499  	var si *api.SystemInfo
   500  	var err error
   501  	s.metrics.ExplorerViews.With(common.Labels{"action": "index"}).Inc()
   502  	si, err = s.api.GetSystemInfo(false)
   503  	if err != nil {
   504  		return errorTpl, nil, err
   505  	}
   506  	data := s.newTemplateData()
   507  	data.Info = si
   508  	return indexTpl, data, nil
   509  }
   510  
   511  func (s *PublicServer) explorerSearch(w http.ResponseWriter, r *http.Request) (tpl, *TemplateData, error) {
   512  	q := strings.TrimSpace(r.URL.Query().Get("q"))
   513  	var tx *api.Tx
   514  	var address *api.Address
   515  	var block *api.Block
   516  	var err error
   517  	s.metrics.ExplorerViews.With(common.Labels{"action": "search"}).Inc()
   518  	if len(q) > 0 {
   519  		block, err = s.api.GetBlock(q, 0, 1)
   520  		if err == nil {
   521  			http.Redirect(w, r, joinURL("/block/", block.Hash), 302)
   522  			return noTpl, nil, nil
   523  		}
   524  		tx, err = s.api.GetTransaction(q, false)
   525  		if err == nil {
   526  			http.Redirect(w, r, joinURL("/tx/", tx.Txid), 302)
   527  			return noTpl, nil, nil
   528  		}
   529  		address, err = s.api.GetAddress(q, 0, 1, true)
   530  		if err == nil {
   531  			http.Redirect(w, r, joinURL("/address/", address.AddrStr), 302)
   532  			return noTpl, nil, nil
   533  		}
   534  	}
   535  	return errorTpl, nil, api.NewAPIError(fmt.Sprintf("No matching records found for '%v'", q), true)
   536  }
   537  
   538  func (s *PublicServer) explorerSendTx(w http.ResponseWriter, r *http.Request) (tpl, *TemplateData, error) {
   539  	s.metrics.ExplorerViews.With(common.Labels{"action": "sendtx"}).Inc()
   540  	data := s.newTemplateData()
   541  	if r.Method == http.MethodPost {
   542  		err := r.ParseForm()
   543  		if err != nil {
   544  			return sendTransactionTpl, data, err
   545  		}
   546  		hex := r.FormValue("hex")
   547  		if len(hex) > 0 {
   548  			res, err := s.chain.SendRawTransaction(hex)
   549  			if err != nil {
   550  				data.SendTxHex = hex
   551  				data.Error = &api.APIError{Text: err.Error(), Public: true}
   552  				return sendTransactionTpl, data, nil
   553  			}
   554  			data.Status = "Transaction sent, result " + res
   555  		}
   556  	}
   557  	return sendTransactionTpl, data, nil
   558  }
   559  
   560  func getPagingRange(page int, total int) ([]int, int, int) {
   561  	if total < 2 {
   562  		return nil, 0, 0
   563  	}
   564  	pp, np := page-1, page+1
   565  	if np > total {
   566  		np = total
   567  	}
   568  	if pp < 1 {
   569  		pp = 1
   570  	}
   571  	r := make([]int, 0, 8)
   572  	if total < 6 {
   573  		for i := 1; i <= total; i++ {
   574  			r = append(r, i)
   575  		}
   576  	} else {
   577  		r = append(r, 1)
   578  		if page > 3 {
   579  			r = append(r, 0)
   580  		}
   581  		if pp == 1 {
   582  			if page == 1 {
   583  				r = append(r, np)
   584  				r = append(r, np+1)
   585  				r = append(r, np+2)
   586  			} else {
   587  				r = append(r, page)
   588  				r = append(r, np)
   589  				r = append(r, np+1)
   590  			}
   591  		} else if np == total {
   592  			if page == total {
   593  				r = append(r, pp-2)
   594  				r = append(r, pp-1)
   595  				r = append(r, pp)
   596  			} else {
   597  				r = append(r, pp-1)
   598  				r = append(r, pp)
   599  				r = append(r, page)
   600  			}
   601  		} else {
   602  			r = append(r, pp)
   603  			r = append(r, page)
   604  			r = append(r, np)
   605  		}
   606  		if page <= total-3 {
   607  			r = append(r, 0)
   608  		}
   609  		r = append(r, total)
   610  	}
   611  	return r, pp, np
   612  }
   613  
   614  func (s *PublicServer) apiIndex(r *http.Request) (interface{}, error) {
   615  	s.metrics.ExplorerViews.With(common.Labels{"action": "api-index"}).Inc()
   616  	return s.api.GetSystemInfo(false)
   617  }
   618  
   619  func (s *PublicServer) apiBlockIndex(r *http.Request) (interface{}, error) {
   620  	type resBlockIndex struct {
   621  		BlockHash string `json:"blockHash"`
   622  	}
   623  	var err error
   624  	var hash string
   625  	height := -1
   626  	if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 {
   627  		if h, err := strconv.Atoi(r.URL.Path[i+1:]); err == nil {
   628  			height = h
   629  		}
   630  	}
   631  	if height >= 0 {
   632  		hash, err = s.db.GetBlockHash(uint32(height))
   633  	} else {
   634  		_, hash, err = s.db.GetBestBlock()
   635  	}
   636  	if err != nil {
   637  		glog.Error(err)
   638  		return nil, err
   639  	}
   640  	return resBlockIndex{
   641  		BlockHash: hash,
   642  	}, nil
   643  }
   644  
   645  func (s *PublicServer) apiTx(r *http.Request) (interface{}, error) {
   646  	var tx *api.Tx
   647  	var err error
   648  	s.metrics.ExplorerViews.With(common.Labels{"action": "api-tx"}).Inc()
   649  	if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 {
   650  		txid := r.URL.Path[i+1:]
   651  		spendingTxs := false
   652  		p := r.URL.Query().Get("spending")
   653  		if len(p) > 0 {
   654  			spendingTxs, err = strconv.ParseBool(p)
   655  			if err != nil {
   656  				return nil, api.NewAPIError("Parameter 'spending' cannot be converted to boolean", true)
   657  			}
   658  		}
   659  		tx, err = s.api.GetTransaction(txid, spendingTxs)
   660  	}
   661  	return tx, err
   662  }
   663  
   664  func (s *PublicServer) apiTxSpecific(r *http.Request) (interface{}, error) {
   665  	var tx json.RawMessage
   666  	var err error
   667  	s.metrics.ExplorerViews.With(common.Labels{"action": "api-tx-specific"}).Inc()
   668  	if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 {
   669  		txid := r.URL.Path[i+1:]
   670  		tx, err = s.chain.GetTransactionSpecific(txid)
   671  	}
   672  	return tx, err
   673  }
   674  
   675  func (s *PublicServer) apiAddress(r *http.Request) (interface{}, error) {
   676  	var address *api.Address
   677  	var err error
   678  	s.metrics.ExplorerViews.With(common.Labels{"action": "api-address"}).Inc()
   679  	if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 {
   680  		page, ec := strconv.Atoi(r.URL.Query().Get("page"))
   681  		if ec != nil {
   682  			page = 0
   683  		}
   684  		address, err = s.api.GetAddress(r.URL.Path[i+1:], page, txsInAPI, true)
   685  	}
   686  	return address, err
   687  }
   688  
   689  func (s *PublicServer) apiAddressUtxo(r *http.Request) (interface{}, error) {
   690  	var utxo []api.AddressUtxo
   691  	var err error
   692  	s.metrics.ExplorerViews.With(common.Labels{"action": "api-address"}).Inc()
   693  	if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 {
   694  		onlyConfirmed := false
   695  		c := r.URL.Query().Get("confirmed")
   696  		if len(c) > 0 {
   697  			onlyConfirmed, err = strconv.ParseBool(c)
   698  			if err != nil {
   699  				return nil, api.NewAPIError("Parameter 'confirmed' cannot be converted to boolean", true)
   700  			}
   701  		}
   702  		utxo, err = s.api.GetAddressUtxo(r.URL.Path[i+1:], onlyConfirmed)
   703  	}
   704  	return utxo, err
   705  }
   706  
   707  func (s *PublicServer) apiBlock(r *http.Request) (interface{}, error) {
   708  	var block *api.Block
   709  	var err error
   710  	s.metrics.ExplorerViews.With(common.Labels{"action": "api-block"}).Inc()
   711  	if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 {
   712  		page, ec := strconv.Atoi(r.URL.Query().Get("page"))
   713  		if ec != nil {
   714  			page = 0
   715  		}
   716  		block, err = s.api.GetBlock(r.URL.Path[i+1:], page, txsInAPI)
   717  	}
   718  	return block, err
   719  }
   720  
   721  type resultSendTransaction struct {
   722  	Result string `json:"result"`
   723  }
   724  
   725  func (s *PublicServer) apiSendTx(r *http.Request) (interface{}, error) {
   726  	var err error
   727  	var res resultSendTransaction
   728  	var hex string
   729  	s.metrics.ExplorerViews.With(common.Labels{"action": "api-sendtx"}).Inc()
   730  	if r.Method == http.MethodPost {
   731  		data, err := ioutil.ReadAll(r.Body)
   732  		if err != nil {
   733  			return nil, api.NewAPIError("Missing tx blob", true)
   734  		}
   735  		hex = string(data)
   736  	} else {
   737  		if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 {
   738  			hex = r.URL.Path[i+1:]
   739  		}
   740  	}
   741  	if len(hex) > 0 {
   742  		res.Result, err = s.chain.SendRawTransaction(hex)
   743  		if err != nil {
   744  			return nil, api.NewAPIError(err.Error(), true)
   745  		}
   746  		return res, nil
   747  	}
   748  	return nil, api.NewAPIError("Missing tx blob", true)
   749  }
   750  
   751  type resultEstimateFeeAsString struct {
   752  	Result string `json:"result"`
   753  }
   754  
   755  func (s *PublicServer) apiEstimateFee(r *http.Request) (interface{}, error) {
   756  	var res resultEstimateFeeAsString
   757  	s.metrics.ExplorerViews.With(common.Labels{"action": "api-estimatefee"}).Inc()
   758  	if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 {
   759  		b := r.URL.Path[i+1:]
   760  		if len(b) > 0 {
   761  			blocks, err := strconv.Atoi(b)
   762  			if err != nil {
   763  				return nil, api.NewAPIError("Parameter 'number of blocks' is not a number", true)
   764  			}
   765  			conservative := true
   766  			c := r.URL.Query().Get("conservative")
   767  			if len(c) > 0 {
   768  				conservative, err = strconv.ParseBool(c)
   769  				if err != nil {
   770  					return nil, api.NewAPIError("Parameter 'conservative' cannot be converted to boolean", true)
   771  				}
   772  			}
   773  			var fee big.Int
   774  			fee, err = s.chain.EstimateSmartFee(blocks, conservative)
   775  			if err != nil {
   776  				fee, err = s.chain.EstimateFee(blocks)
   777  				if err != nil {
   778  					return nil, err
   779  				}
   780  			}
   781  			res.Result = s.chainParser.AmountToDecimalString(&fee)
   782  			return res, nil
   783  		}
   784  	}
   785  	return nil, api.NewAPIError("Missing parameter 'number of blocks'", true)
   786  }