github.com/rumhocker/blockbook@v0.3.2/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  	"path/filepath"
    16  	"reflect"
    17  	"regexp"
    18  	"runtime"
    19  	"runtime/debug"
    20  	"strconv"
    21  	"strings"
    22  	"time"
    23  
    24  	"github.com/golang/glog"
    25  )
    26  
    27  const txsOnPage = 25
    28  const blocksOnPage = 50
    29  const mempoolTxsOnPage = 50
    30  const txsInAPI = 1000
    31  
    32  const (
    33  	_ = iota
    34  	apiV1
    35  	apiV2
    36  )
    37  
    38  // PublicServer is a handle to public http server
    39  type PublicServer struct {
    40  	binding          string
    41  	certFiles        string
    42  	socketio         *SocketIoServer
    43  	websocket        *WebsocketServer
    44  	https            *http.Server
    45  	db               *db.RocksDB
    46  	txCache          *db.TxCache
    47  	chain            bchain.BlockChain
    48  	chainParser      bchain.BlockChainParser
    49  	mempool          bchain.Mempool
    50  	api              *api.Worker
    51  	explorerURL      string
    52  	internalExplorer bool
    53  	metrics          *common.Metrics
    54  	is               *common.InternalState
    55  	templates        []*template.Template
    56  	debug            bool
    57  }
    58  
    59  // NewPublicServer creates new public server http interface to blockbook and returns its handle
    60  // only basic functionality is mapped, to map all functions, call
    61  func NewPublicServer(binding string, certFiles string, db *db.RocksDB, chain bchain.BlockChain, mempool bchain.Mempool, txCache *db.TxCache, explorerURL string, metrics *common.Metrics, is *common.InternalState, debugMode bool) (*PublicServer, error) {
    62  
    63  	api, err := api.NewWorker(db, chain, mempool, txCache, is)
    64  	if err != nil {
    65  		return nil, err
    66  	}
    67  
    68  	socketio, err := NewSocketIoServer(db, chain, mempool, txCache, metrics, is)
    69  	if err != nil {
    70  		return nil, err
    71  	}
    72  
    73  	websocket, err := NewWebsocketServer(db, chain, mempool, txCache, metrics, is)
    74  	if err != nil {
    75  		return nil, err
    76  	}
    77  
    78  	addr, path := splitBinding(binding)
    79  	serveMux := http.NewServeMux()
    80  	https := &http.Server{
    81  		Addr:    addr,
    82  		Handler: serveMux,
    83  	}
    84  
    85  	s := &PublicServer{
    86  		binding:          binding,
    87  		certFiles:        certFiles,
    88  		https:            https,
    89  		api:              api,
    90  		socketio:         socketio,
    91  		websocket:        websocket,
    92  		db:               db,
    93  		txCache:          txCache,
    94  		chain:            chain,
    95  		chainParser:      chain.GetChainParser(),
    96  		mempool:          mempool,
    97  		explorerURL:      explorerURL,
    98  		internalExplorer: explorerURL == "",
    99  		metrics:          metrics,
   100  		is:               is,
   101  		debug:            debugMode,
   102  	}
   103  	s.templates = s.parseTemplates()
   104  
   105  	// map only basic functions, the rest is enabled by method MapFullPublicInterface
   106  	serveMux.Handle(path+"favicon.ico", http.FileServer(http.Dir("./static/")))
   107  	serveMux.Handle(path+"static/", http.StripPrefix("/static/", http.FileServer(http.Dir("./static/"))))
   108  	// default handler
   109  	serveMux.HandleFunc(path, s.htmlTemplateHandler(s.explorerIndex))
   110  	// default API handler
   111  	serveMux.HandleFunc(path+"api/", s.jsonHandler(s.apiIndex, apiV2))
   112  
   113  	return s, nil
   114  }
   115  
   116  // Run starts the server
   117  func (s *PublicServer) Run() error {
   118  	if s.certFiles == "" {
   119  		glog.Info("public server: starting to listen on http://", s.https.Addr)
   120  		return s.https.ListenAndServe()
   121  	}
   122  	glog.Info("public server starting to listen on https://", s.https.Addr)
   123  	return s.https.ListenAndServeTLS(fmt.Sprint(s.certFiles, ".crt"), fmt.Sprint(s.certFiles, ".key"))
   124  }
   125  
   126  // ConnectFullPublicInterface enables complete public functionality
   127  func (s *PublicServer) ConnectFullPublicInterface() {
   128  	serveMux := s.https.Handler.(*http.ServeMux)
   129  	_, path := splitBinding(s.binding)
   130  	// support for test pages
   131  	serveMux.Handle(path+"test-socketio.html", http.FileServer(http.Dir("./static/")))
   132  	serveMux.Handle(path+"test-websocket.html", http.FileServer(http.Dir("./static/")))
   133  	if s.internalExplorer {
   134  		// internal explorer handlers
   135  		serveMux.HandleFunc(path+"tx/", s.htmlTemplateHandler(s.explorerTx))
   136  		serveMux.HandleFunc(path+"address/", s.htmlTemplateHandler(s.explorerAddress))
   137  		serveMux.HandleFunc(path+"xpub/", s.htmlTemplateHandler(s.explorerXpub))
   138  		serveMux.HandleFunc(path+"search/", s.htmlTemplateHandler(s.explorerSearch))
   139  		serveMux.HandleFunc(path+"blocks", s.htmlTemplateHandler(s.explorerBlocks))
   140  		serveMux.HandleFunc(path+"block/", s.htmlTemplateHandler(s.explorerBlock))
   141  		serveMux.HandleFunc(path+"spending/", s.htmlTemplateHandler(s.explorerSpendingTx))
   142  		serveMux.HandleFunc(path+"sendtx", s.htmlTemplateHandler(s.explorerSendTx))
   143  		serveMux.HandleFunc(path+"mempool", s.htmlTemplateHandler(s.explorerMempool))
   144  	} else {
   145  		// redirect to wallet requests for tx and address, possibly to external site
   146  		serveMux.HandleFunc(path+"tx/", s.txRedirect)
   147  		serveMux.HandleFunc(path+"address/", s.addressRedirect)
   148  	}
   149  	// API calls
   150  	// default api without version can be changed to different version at any time
   151  	// use versioned api for stability
   152  
   153  	var apiDefault int
   154  	// ethereum supports only api V2
   155  	if s.chainParser.GetChainType() == bchain.ChainEthereumType {
   156  		apiDefault = apiV2
   157  	} else {
   158  		apiDefault = apiV1
   159  		// legacy v1 format
   160  		serveMux.HandleFunc(path+"api/v1/block-index/", s.jsonHandler(s.apiBlockIndex, apiV1))
   161  		serveMux.HandleFunc(path+"api/v1/tx-specific/", s.jsonHandler(s.apiTxSpecific, apiV1))
   162  		serveMux.HandleFunc(path+"api/v1/tx/", s.jsonHandler(s.apiTx, apiV1))
   163  		serveMux.HandleFunc(path+"api/v1/address/", s.jsonHandler(s.apiAddress, apiV1))
   164  		serveMux.HandleFunc(path+"api/v1/utxo/", s.jsonHandler(s.apiUtxo, apiV1))
   165  		serveMux.HandleFunc(path+"api/v1/block/", s.jsonHandler(s.apiBlock, apiV1))
   166  		serveMux.HandleFunc(path+"api/v1/sendtx/", s.jsonHandler(s.apiSendTx, apiV1))
   167  		serveMux.HandleFunc(path+"api/v1/estimatefee/", s.jsonHandler(s.apiEstimateFee, apiV1))
   168  	}
   169  	serveMux.HandleFunc(path+"api/block-index/", s.jsonHandler(s.apiBlockIndex, apiDefault))
   170  	serveMux.HandleFunc(path+"api/tx-specific/", s.jsonHandler(s.apiTxSpecific, apiDefault))
   171  	serveMux.HandleFunc(path+"api/tx/", s.jsonHandler(s.apiTx, apiDefault))
   172  	serveMux.HandleFunc(path+"api/address/", s.jsonHandler(s.apiAddress, apiDefault))
   173  	serveMux.HandleFunc(path+"api/xpub/", s.jsonHandler(s.apiXpub, apiDefault))
   174  	serveMux.HandleFunc(path+"api/utxo/", s.jsonHandler(s.apiUtxo, apiDefault))
   175  	serveMux.HandleFunc(path+"api/block/", s.jsonHandler(s.apiBlock, apiDefault))
   176  	serveMux.HandleFunc(path+"api/sendtx/", s.jsonHandler(s.apiSendTx, apiDefault))
   177  	serveMux.HandleFunc(path+"api/estimatefee/", s.jsonHandler(s.apiEstimateFee, apiDefault))
   178  	serveMux.HandleFunc(path+"api/balancehistory/", s.jsonHandler(s.apiBalanceHistory, apiDefault))
   179  	// v2 format
   180  	serveMux.HandleFunc(path+"api/v2/block-index/", s.jsonHandler(s.apiBlockIndex, apiV2))
   181  	serveMux.HandleFunc(path+"api/v2/tx-specific/", s.jsonHandler(s.apiTxSpecific, apiV2))
   182  	serveMux.HandleFunc(path+"api/v2/tx/", s.jsonHandler(s.apiTx, apiV2))
   183  	serveMux.HandleFunc(path+"api/v2/address/", s.jsonHandler(s.apiAddress, apiV2))
   184  	serveMux.HandleFunc(path+"api/v2/xpub/", s.jsonHandler(s.apiXpub, apiV2))
   185  	serveMux.HandleFunc(path+"api/v2/utxo/", s.jsonHandler(s.apiUtxo, apiV2))
   186  	serveMux.HandleFunc(path+"api/v2/block/", s.jsonHandler(s.apiBlock, apiV2))
   187  	serveMux.HandleFunc(path+"api/v2/sendtx/", s.jsonHandler(s.apiSendTx, apiV2))
   188  	serveMux.HandleFunc(path+"api/v2/estimatefee/", s.jsonHandler(s.apiEstimateFee, apiV2))
   189  	serveMux.HandleFunc(path+"api/v2/feestats/", s.jsonHandler(s.apiFeeStats, apiV2))
   190  	serveMux.HandleFunc(path+"api/v2/balancehistory/", s.jsonHandler(s.apiBalanceHistory, apiDefault))
   191  	serveMux.HandleFunc(path+"api/v2/tickers/", s.jsonHandler(s.apiTickers, apiV2))
   192  	serveMux.HandleFunc(path+"api/v2/tickers-list/", s.jsonHandler(s.apiTickersList, apiV2))
   193  	// socket.io interface
   194  	serveMux.Handle(path+"socket.io/", s.socketio.GetHandler())
   195  	// websocket interface
   196  	serveMux.Handle(path+"websocket", s.websocket.GetHandler())
   197  }
   198  
   199  // Close closes the server
   200  func (s *PublicServer) Close() error {
   201  	glog.Infof("public server: closing")
   202  	return s.https.Close()
   203  }
   204  
   205  // Shutdown shuts down the server
   206  func (s *PublicServer) Shutdown(ctx context.Context) error {
   207  	glog.Infof("public server: shutdown")
   208  	return s.https.Shutdown(ctx)
   209  }
   210  
   211  // OnNewBlock notifies users subscribed to bitcoind/hashblock about new block
   212  func (s *PublicServer) OnNewBlock(hash string, height uint32) {
   213  	s.socketio.OnNewBlockHash(hash)
   214  	s.websocket.OnNewBlock(hash, height)
   215  }
   216  
   217  // OnNewFiatRatesTicker notifies users subscribed to bitcoind/fiatrates about new ticker
   218  func (s *PublicServer) OnNewFiatRatesTicker(ticker *db.CurrencyRatesTicker) {
   219  	s.websocket.OnNewFiatRatesTicker(ticker)
   220  }
   221  
   222  // OnNewTxAddr notifies users subscribed to bitcoind/addresstxid about new block
   223  func (s *PublicServer) OnNewTxAddr(tx *bchain.Tx, desc bchain.AddressDescriptor) {
   224  	s.socketio.OnNewTxAddr(tx.Txid, desc)
   225  	s.websocket.OnNewTxAddr(tx, desc)
   226  }
   227  
   228  func (s *PublicServer) txRedirect(w http.ResponseWriter, r *http.Request) {
   229  	http.Redirect(w, r, joinURL(s.explorerURL, r.URL.Path), 302)
   230  	s.metrics.ExplorerViews.With(common.Labels{"action": "tx-redirect"}).Inc()
   231  }
   232  
   233  func (s *PublicServer) addressRedirect(w http.ResponseWriter, r *http.Request) {
   234  	http.Redirect(w, r, joinURL(s.explorerURL, r.URL.Path), 302)
   235  	s.metrics.ExplorerViews.With(common.Labels{"action": "address-redirect"}).Inc()
   236  }
   237  
   238  func splitBinding(binding string) (addr string, path string) {
   239  	i := strings.Index(binding, "/")
   240  	if i >= 0 {
   241  		return binding[0:i], binding[i:]
   242  	}
   243  	return binding, "/"
   244  }
   245  
   246  func joinURL(base string, part string) string {
   247  	if len(base) > 0 {
   248  		if len(base) > 0 && base[len(base)-1] == '/' && len(part) > 0 && part[0] == '/' {
   249  			return base + part[1:]
   250  		}
   251  		return base + part
   252  	}
   253  	return part
   254  }
   255  
   256  func getFunctionName(i interface{}) string {
   257  	return runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name()
   258  }
   259  
   260  func (s *PublicServer) jsonHandler(handler func(r *http.Request, apiVersion int) (interface{}, error), apiVersion int) func(w http.ResponseWriter, r *http.Request) {
   261  	type jsonError struct {
   262  		Text       string `json:"error"`
   263  		HTTPStatus int    `json:"-"`
   264  	}
   265  	return func(w http.ResponseWriter, r *http.Request) {
   266  		var data interface{}
   267  		var err error
   268  		defer func() {
   269  			if e := recover(); e != nil {
   270  				glog.Error(getFunctionName(handler), " recovered from panic: ", e)
   271  				debug.PrintStack()
   272  				if s.debug {
   273  					data = jsonError{fmt.Sprint("Internal server error: recovered from panic ", e), http.StatusInternalServerError}
   274  				} else {
   275  					data = jsonError{"Internal server error", http.StatusInternalServerError}
   276  				}
   277  			}
   278  			w.Header().Set("Content-Type", "application/json; charset=utf-8")
   279  			if e, isError := data.(jsonError); isError {
   280  				w.WriteHeader(e.HTTPStatus)
   281  			}
   282  			err = json.NewEncoder(w).Encode(data)
   283  			if err != nil {
   284  				glog.Warning("json encode ", err)
   285  			}
   286  		}()
   287  		data, err = handler(r, apiVersion)
   288  		if err != nil || data == nil {
   289  			if apiErr, ok := err.(*api.APIError); ok {
   290  				if apiErr.Public {
   291  					data = jsonError{apiErr.Error(), http.StatusBadRequest}
   292  				} else {
   293  					data = jsonError{apiErr.Error(), http.StatusInternalServerError}
   294  				}
   295  			} else {
   296  				if err != nil {
   297  					glog.Error(getFunctionName(handler), " error: ", err)
   298  				}
   299  				if s.debug {
   300  					if data != nil {
   301  						data = jsonError{fmt.Sprintf("Internal server error: %v, data %+v", err, data), http.StatusInternalServerError}
   302  					} else {
   303  						data = jsonError{fmt.Sprintf("Internal server error: %v", err), http.StatusInternalServerError}
   304  					}
   305  				} else {
   306  					data = jsonError{"Internal server error", http.StatusInternalServerError}
   307  				}
   308  			}
   309  		}
   310  	}
   311  }
   312  
   313  func (s *PublicServer) newTemplateData() *TemplateData {
   314  	return &TemplateData{
   315  		CoinName:         s.is.Coin,
   316  		CoinShortcut:     s.is.CoinShortcut,
   317  		CoinLabel:        s.is.CoinLabel,
   318  		ChainType:        s.chainParser.GetChainType(),
   319  		InternalExplorer: s.internalExplorer && !s.is.InitialSync,
   320  		TOSLink:          api.Text.TOSLink,
   321  	}
   322  }
   323  
   324  func (s *PublicServer) newTemplateDataWithError(text string) *TemplateData {
   325  	td := s.newTemplateData()
   326  	td.Error = &api.APIError{Text: text}
   327  	return td
   328  }
   329  
   330  func (s *PublicServer) htmlTemplateHandler(handler func(w http.ResponseWriter, r *http.Request) (tpl, *TemplateData, error)) func(w http.ResponseWriter, r *http.Request) {
   331  	return func(w http.ResponseWriter, r *http.Request) {
   332  		var t tpl
   333  		var data *TemplateData
   334  		var err error
   335  		defer func() {
   336  			if e := recover(); e != nil {
   337  				glog.Error(getFunctionName(handler), " recovered from panic: ", e)
   338  				debug.PrintStack()
   339  				t = errorInternalTpl
   340  				if s.debug {
   341  					data = s.newTemplateDataWithError(fmt.Sprint("Internal server error: recovered from panic ", e))
   342  				} else {
   343  					data = s.newTemplateDataWithError("Internal server error")
   344  				}
   345  			}
   346  			// noTpl means the handler completely handled the request
   347  			if t != noTpl {
   348  				w.Header().Set("Content-Type", "text/html; charset=utf-8")
   349  				// return 500 Internal Server Error with errorInternalTpl
   350  				if t == errorInternalTpl {
   351  					w.WriteHeader(http.StatusInternalServerError)
   352  				}
   353  				if err := s.templates[t].ExecuteTemplate(w, "base.html", data); err != nil {
   354  					glog.Error(err)
   355  				}
   356  			}
   357  		}()
   358  		if s.debug {
   359  			// reload templates on each request
   360  			// to reflect changes during development
   361  			s.templates = s.parseTemplates()
   362  		}
   363  		t, data, err = handler(w, r)
   364  		if err != nil || (data == nil && t != noTpl) {
   365  			t = errorInternalTpl
   366  			if apiErr, ok := err.(*api.APIError); ok {
   367  				data = s.newTemplateData()
   368  				data.Error = apiErr
   369  				if apiErr.Public {
   370  					t = errorTpl
   371  				}
   372  			} else {
   373  				if err != nil {
   374  					glog.Error(getFunctionName(handler), " error: ", err)
   375  				}
   376  				if s.debug {
   377  					data = s.newTemplateDataWithError(fmt.Sprintf("Internal server error: %v, data %+v", err, data))
   378  				} else {
   379  					data = s.newTemplateDataWithError("Internal server error")
   380  				}
   381  			}
   382  		}
   383  	}
   384  }
   385  
   386  type tpl int
   387  
   388  const (
   389  	noTpl = tpl(iota)
   390  	errorTpl
   391  	errorInternalTpl
   392  	indexTpl
   393  	txTpl
   394  	addressTpl
   395  	xpubTpl
   396  	blocksTpl
   397  	blockTpl
   398  	sendTransactionTpl
   399  	mempoolTpl
   400  
   401  	tplCount
   402  )
   403  
   404  // TemplateData is used to transfer data to the templates
   405  type TemplateData struct {
   406  	CoinName             string
   407  	CoinShortcut         string
   408  	CoinLabel            string
   409  	InternalExplorer     bool
   410  	ChainType            bchain.ChainType
   411  	Address              *api.Address
   412  	AddrStr              string
   413  	Tx                   *api.Tx
   414  	Error                *api.APIError
   415  	Blocks               *api.Blocks
   416  	Block                *api.Block
   417  	Info                 *api.SystemInfo
   418  	MempoolTxids         *api.MempoolTxids
   419  	Page                 int
   420  	PrevPage             int
   421  	NextPage             int
   422  	PagingRange          []int
   423  	PageParams           template.URL
   424  	TOSLink              string
   425  	SendTxHex            string
   426  	Status               string
   427  	NonZeroBalanceTokens bool
   428  }
   429  
   430  func (s *PublicServer) parseTemplates() []*template.Template {
   431  	templateFuncMap := template.FuncMap{
   432  		"formatTime":               formatTime,
   433  		"formatUnixTime":           formatUnixTime,
   434  		"formatAmount":             s.formatAmount,
   435  		"formatAmountWithDecimals": formatAmountWithDecimals,
   436  		"setTxToTemplateData":      setTxToTemplateData,
   437  		"isOwnAddress":             isOwnAddress,
   438  		"isOwnAddresses":           isOwnAddresses,
   439  	}
   440  	var createTemplate func(filenames ...string) *template.Template
   441  	if s.debug {
   442  		createTemplate = func(filenames ...string) *template.Template {
   443  			if len(filenames) == 0 {
   444  				panic("Missing templates")
   445  			}
   446  			return template.Must(template.New(filepath.Base(filenames[0])).Funcs(templateFuncMap).ParseFiles(filenames...))
   447  		}
   448  	} else {
   449  		createTemplate = func(filenames ...string) *template.Template {
   450  			if len(filenames) == 0 {
   451  				panic("Missing templates")
   452  			}
   453  			t := template.New(filepath.Base(filenames[0])).Funcs(templateFuncMap)
   454  			for _, filename := range filenames {
   455  				b, err := ioutil.ReadFile(filename)
   456  				if err != nil {
   457  					panic(err)
   458  				}
   459  				// perform very simple minification - replace leading spaces used as formatting and new lines
   460  				r := regexp.MustCompile(`\n\s*`)
   461  				b = r.ReplaceAll(b, []byte{})
   462  				s := string(b)
   463  				name := filepath.Base(filename)
   464  				var tt *template.Template
   465  				if name == t.Name() {
   466  					tt = t
   467  				} else {
   468  					tt = t.New(name)
   469  				}
   470  				_, err = tt.Parse(s)
   471  				if err != nil {
   472  					panic(err)
   473  				}
   474  			}
   475  			return t
   476  		}
   477  	}
   478  	t := make([]*template.Template, tplCount)
   479  	t[errorTpl] = createTemplate("./static/templates/error.html", "./static/templates/base.html")
   480  	t[errorInternalTpl] = createTemplate("./static/templates/error.html", "./static/templates/base.html")
   481  	t[indexTpl] = createTemplate("./static/templates/index.html", "./static/templates/base.html")
   482  	t[blocksTpl] = createTemplate("./static/templates/blocks.html", "./static/templates/paging.html", "./static/templates/base.html")
   483  	t[sendTransactionTpl] = createTemplate("./static/templates/sendtx.html", "./static/templates/base.html")
   484  	if s.chainParser.GetChainType() == bchain.ChainEthereumType {
   485  		t[txTpl] = createTemplate("./static/templates/tx.html", "./static/templates/txdetail_ethereumtype.html", "./static/templates/base.html")
   486  		t[addressTpl] = createTemplate("./static/templates/address.html", "./static/templates/txdetail_ethereumtype.html", "./static/templates/paging.html", "./static/templates/base.html")
   487  		t[blockTpl] = createTemplate("./static/templates/block.html", "./static/templates/txdetail_ethereumtype.html", "./static/templates/paging.html", "./static/templates/base.html")
   488  	} else {
   489  		t[txTpl] = createTemplate("./static/templates/tx.html", "./static/templates/txdetail.html", "./static/templates/base.html")
   490  		t[addressTpl] = createTemplate("./static/templates/address.html", "./static/templates/txdetail.html", "./static/templates/paging.html", "./static/templates/base.html")
   491  		t[blockTpl] = createTemplate("./static/templates/block.html", "./static/templates/txdetail.html", "./static/templates/paging.html", "./static/templates/base.html")
   492  	}
   493  	t[xpubTpl] = createTemplate("./static/templates/xpub.html", "./static/templates/txdetail.html", "./static/templates/paging.html", "./static/templates/base.html")
   494  	t[mempoolTpl] = createTemplate("./static/templates/mempool.html", "./static/templates/paging.html", "./static/templates/base.html")
   495  	return t
   496  }
   497  
   498  func formatUnixTime(ut int64) string {
   499  	return formatTime(time.Unix(ut, 0))
   500  }
   501  
   502  func formatTime(t time.Time) string {
   503  	return t.Format(time.RFC1123)
   504  }
   505  
   506  // for now return the string as it is
   507  // in future could be used to do coin specific formatting
   508  func (s *PublicServer) formatAmount(a *api.Amount) string {
   509  	if a == nil {
   510  		return "0"
   511  	}
   512  	return s.chainParser.AmountToDecimalString((*big.Int)(a))
   513  }
   514  
   515  func formatAmountWithDecimals(a *api.Amount, d int) string {
   516  	if a == nil {
   517  		return "0"
   518  	}
   519  	return a.DecimalString(d)
   520  }
   521  
   522  // called from template to support txdetail.html functionality
   523  func setTxToTemplateData(td *TemplateData, tx *api.Tx) *TemplateData {
   524  	td.Tx = tx
   525  	return td
   526  }
   527  
   528  // returns true if address is "own",
   529  // i.e. either the address of the address detail or belonging to the xpub
   530  func isOwnAddress(td *TemplateData, a string) bool {
   531  	if a == td.AddrStr {
   532  		return true
   533  	}
   534  	if td.Address != nil && td.Address.XPubAddresses != nil {
   535  		if _, found := td.Address.XPubAddresses[a]; found {
   536  			return true
   537  		}
   538  	}
   539  	return false
   540  }
   541  
   542  // returns true if addresses are "own",
   543  // i.e. either the address of the address detail or belonging to the xpub
   544  func isOwnAddresses(td *TemplateData, addresses []string) bool {
   545  	if len(addresses) == 1 {
   546  		return isOwnAddress(td, addresses[0])
   547  	}
   548  	return false
   549  }
   550  
   551  func (s *PublicServer) explorerTx(w http.ResponseWriter, r *http.Request) (tpl, *TemplateData, error) {
   552  	var tx *api.Tx
   553  	var err error
   554  	s.metrics.ExplorerViews.With(common.Labels{"action": "tx"}).Inc()
   555  	if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 {
   556  		txid := r.URL.Path[i+1:]
   557  		tx, err = s.api.GetTransaction(txid, false, true)
   558  		if err != nil {
   559  			return errorTpl, nil, err
   560  		}
   561  	}
   562  	data := s.newTemplateData()
   563  	data.Tx = tx
   564  	return txTpl, data, nil
   565  }
   566  
   567  func (s *PublicServer) explorerSpendingTx(w http.ResponseWriter, r *http.Request) (tpl, *TemplateData, error) {
   568  	s.metrics.ExplorerViews.With(common.Labels{"action": "spendingtx"}).Inc()
   569  	var err error
   570  	parts := strings.Split(r.URL.Path, "/")
   571  	if len(parts) > 2 {
   572  		tx := parts[len(parts)-2]
   573  		n, ec := strconv.Atoi(parts[len(parts)-1])
   574  		if ec == nil {
   575  			spendingTx, err := s.api.GetSpendingTxid(tx, n)
   576  			if err == nil && spendingTx != "" {
   577  				http.Redirect(w, r, joinURL("/tx/", spendingTx), 302)
   578  				return noTpl, nil, nil
   579  			}
   580  		}
   581  	}
   582  	if err == nil {
   583  		err = api.NewAPIError("Transaction not found", true)
   584  	}
   585  	return errorTpl, nil, err
   586  }
   587  
   588  func (s *PublicServer) getAddressQueryParams(r *http.Request, accountDetails api.AccountDetails, maxPageSize int) (int, int, api.AccountDetails, *api.AddressFilter, string, int) {
   589  	var voutFilter = api.AddressFilterVoutOff
   590  	page, ec := strconv.Atoi(r.URL.Query().Get("page"))
   591  	if ec != nil {
   592  		page = 0
   593  	}
   594  	pageSize, ec := strconv.Atoi(r.URL.Query().Get("pageSize"))
   595  	if ec != nil || pageSize > maxPageSize {
   596  		pageSize = maxPageSize
   597  	}
   598  	from, ec := strconv.Atoi(r.URL.Query().Get("from"))
   599  	if ec != nil {
   600  		from = 0
   601  	}
   602  	to, ec := strconv.Atoi(r.URL.Query().Get("to"))
   603  	if ec != nil {
   604  		to = 0
   605  	}
   606  	filterParam := r.URL.Query().Get("filter")
   607  	if len(filterParam) > 0 {
   608  		if filterParam == "inputs" {
   609  			voutFilter = api.AddressFilterVoutInputs
   610  		} else if filterParam == "outputs" {
   611  			voutFilter = api.AddressFilterVoutOutputs
   612  		} else {
   613  			voutFilter, ec = strconv.Atoi(filterParam)
   614  			if ec != nil || voutFilter < 0 {
   615  				voutFilter = api.AddressFilterVoutOff
   616  			}
   617  		}
   618  	}
   619  	switch r.URL.Query().Get("details") {
   620  	case "basic":
   621  		accountDetails = api.AccountDetailsBasic
   622  	case "tokens":
   623  		accountDetails = api.AccountDetailsTokens
   624  	case "tokenBalances":
   625  		accountDetails = api.AccountDetailsTokenBalances
   626  	case "txids":
   627  		accountDetails = api.AccountDetailsTxidHistory
   628  	case "txslight":
   629  		accountDetails = api.AccountDetailsTxHistoryLight
   630  	case "txs":
   631  		accountDetails = api.AccountDetailsTxHistory
   632  	}
   633  	tokensToReturn := api.TokensToReturnNonzeroBalance
   634  	switch r.URL.Query().Get("tokens") {
   635  	case "derived":
   636  		tokensToReturn = api.TokensToReturnDerived
   637  	case "used":
   638  		tokensToReturn = api.TokensToReturnUsed
   639  	case "nonzero":
   640  		tokensToReturn = api.TokensToReturnNonzeroBalance
   641  	}
   642  	gap, ec := strconv.Atoi(r.URL.Query().Get("gap"))
   643  	if ec != nil {
   644  		gap = 0
   645  	}
   646  	contract := r.URL.Query().Get("contract")
   647  	return page, pageSize, accountDetails, &api.AddressFilter{
   648  		Vout:           voutFilter,
   649  		TokensToReturn: tokensToReturn,
   650  		FromHeight:     uint32(from),
   651  		ToHeight:       uint32(to),
   652  		Contract:       contract,
   653  	}, filterParam, gap
   654  }
   655  
   656  func (s *PublicServer) explorerAddress(w http.ResponseWriter, r *http.Request) (tpl, *TemplateData, error) {
   657  	var addressParam string
   658  	i := strings.LastIndexByte(r.URL.Path, '/')
   659  	if i > 0 {
   660  		addressParam = r.URL.Path[i+1:]
   661  	}
   662  	if len(addressParam) == 0 {
   663  		return errorTpl, nil, api.NewAPIError("Missing address", true)
   664  	}
   665  	s.metrics.ExplorerViews.With(common.Labels{"action": "address"}).Inc()
   666  	page, _, _, filter, filterParam, _ := s.getAddressQueryParams(r, api.AccountDetailsTxHistoryLight, txsOnPage)
   667  	// do not allow details to be changed by query params
   668  	address, err := s.api.GetAddress(addressParam, page, txsOnPage, api.AccountDetailsTxHistoryLight, filter)
   669  	if err != nil {
   670  		return errorTpl, nil, err
   671  	}
   672  	data := s.newTemplateData()
   673  	data.AddrStr = address.AddrStr
   674  	data.Address = address
   675  	data.Page = address.Page
   676  	data.PagingRange, data.PrevPage, data.NextPage = getPagingRange(address.Page, address.TotalPages)
   677  	if filterParam == "" && filter.Vout > -1 {
   678  		filterParam = strconv.Itoa(filter.Vout)
   679  	}
   680  	if filterParam != "" {
   681  		data.PageParams = template.URL("&filter=" + filterParam)
   682  		data.Address.Filter = filterParam
   683  	}
   684  	return addressTpl, data, nil
   685  }
   686  
   687  func (s *PublicServer) explorerXpub(w http.ResponseWriter, r *http.Request) (tpl, *TemplateData, error) {
   688  	var xpub string
   689  	i := strings.LastIndexByte(r.URL.Path, '/')
   690  	if i > 0 {
   691  		xpub = r.URL.Path[i+1:]
   692  	}
   693  	if len(xpub) == 0 {
   694  		return errorTpl, nil, api.NewAPIError("Missing xpub", true)
   695  	}
   696  	s.metrics.ExplorerViews.With(common.Labels{"action": "xpub"}).Inc()
   697  	page, _, _, filter, filterParam, gap := s.getAddressQueryParams(r, api.AccountDetailsTxHistoryLight, txsOnPage)
   698  	// do not allow txsOnPage and details to be changed by query params
   699  	address, err := s.api.GetXpubAddress(xpub, page, txsOnPage, api.AccountDetailsTxHistoryLight, filter, gap)
   700  	if err != nil {
   701  		if err == api.ErrUnsupportedXpub {
   702  			err = api.NewAPIError("XPUB functionality is not supported", true)
   703  		}
   704  		return errorTpl, nil, err
   705  	}
   706  	data := s.newTemplateData()
   707  	data.AddrStr = address.AddrStr
   708  	data.Address = address
   709  	data.Page = address.Page
   710  	data.PagingRange, data.PrevPage, data.NextPage = getPagingRange(address.Page, address.TotalPages)
   711  	if filterParam != "" {
   712  		data.PageParams = template.URL("&filter=" + filterParam)
   713  		data.Address.Filter = filterParam
   714  	}
   715  	data.NonZeroBalanceTokens = filter.TokensToReturn == api.TokensToReturnNonzeroBalance
   716  	return xpubTpl, data, nil
   717  }
   718  
   719  func (s *PublicServer) explorerBlocks(w http.ResponseWriter, r *http.Request) (tpl, *TemplateData, error) {
   720  	var blocks *api.Blocks
   721  	var err error
   722  	s.metrics.ExplorerViews.With(common.Labels{"action": "blocks"}).Inc()
   723  	page, ec := strconv.Atoi(r.URL.Query().Get("page"))
   724  	if ec != nil {
   725  		page = 0
   726  	}
   727  	blocks, err = s.api.GetBlocks(page, blocksOnPage)
   728  	if err != nil {
   729  		return errorTpl, nil, err
   730  	}
   731  	data := s.newTemplateData()
   732  	data.Blocks = blocks
   733  	data.Page = blocks.Page
   734  	data.PagingRange, data.PrevPage, data.NextPage = getPagingRange(blocks.Page, blocks.TotalPages)
   735  	return blocksTpl, data, nil
   736  }
   737  
   738  func (s *PublicServer) explorerBlock(w http.ResponseWriter, r *http.Request) (tpl, *TemplateData, error) {
   739  	var block *api.Block
   740  	var err error
   741  	s.metrics.ExplorerViews.With(common.Labels{"action": "block"}).Inc()
   742  	if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 {
   743  		page, ec := strconv.Atoi(r.URL.Query().Get("page"))
   744  		if ec != nil {
   745  			page = 0
   746  		}
   747  		block, err = s.api.GetBlock(r.URL.Path[i+1:], page, txsOnPage)
   748  		if err != nil {
   749  			return errorTpl, nil, err
   750  		}
   751  	}
   752  	data := s.newTemplateData()
   753  	data.Block = block
   754  	data.Page = block.Page
   755  	data.PagingRange, data.PrevPage, data.NextPage = getPagingRange(block.Page, block.TotalPages)
   756  	return blockTpl, data, nil
   757  }
   758  
   759  func (s *PublicServer) explorerIndex(w http.ResponseWriter, r *http.Request) (tpl, *TemplateData, error) {
   760  	var si *api.SystemInfo
   761  	var err error
   762  	s.metrics.ExplorerViews.With(common.Labels{"action": "index"}).Inc()
   763  	si, err = s.api.GetSystemInfo(false)
   764  	if err != nil {
   765  		return errorTpl, nil, err
   766  	}
   767  	data := s.newTemplateData()
   768  	data.Info = si
   769  	return indexTpl, data, nil
   770  }
   771  
   772  func (s *PublicServer) explorerSearch(w http.ResponseWriter, r *http.Request) (tpl, *TemplateData, error) {
   773  	q := strings.TrimSpace(r.URL.Query().Get("q"))
   774  	var tx *api.Tx
   775  	var address *api.Address
   776  	var block *api.Block
   777  	var err error
   778  	s.metrics.ExplorerViews.With(common.Labels{"action": "search"}).Inc()
   779  	if len(q) > 0 {
   780  		address, err = s.api.GetXpubAddress(q, 0, 1, api.AccountDetailsBasic, &api.AddressFilter{Vout: api.AddressFilterVoutOff}, 0)
   781  		if err == nil {
   782  			http.Redirect(w, r, joinURL("/xpub/", address.AddrStr), 302)
   783  			return noTpl, nil, nil
   784  		}
   785  		block, err = s.api.GetBlock(q, 0, 1)
   786  		if err == nil {
   787  			http.Redirect(w, r, joinURL("/block/", block.Hash), 302)
   788  			return noTpl, nil, nil
   789  		}
   790  		tx, err = s.api.GetTransaction(q, false, false)
   791  		if err == nil {
   792  			http.Redirect(w, r, joinURL("/tx/", tx.Txid), 302)
   793  			return noTpl, nil, nil
   794  		}
   795  		address, err = s.api.GetAddress(q, 0, 1, api.AccountDetailsBasic, &api.AddressFilter{Vout: api.AddressFilterVoutOff})
   796  		if err == nil {
   797  			http.Redirect(w, r, joinURL("/address/", address.AddrStr), 302)
   798  			return noTpl, nil, nil
   799  		}
   800  	}
   801  	return errorTpl, nil, api.NewAPIError(fmt.Sprintf("No matching records found for '%v'", q), true)
   802  }
   803  
   804  func (s *PublicServer) explorerSendTx(w http.ResponseWriter, r *http.Request) (tpl, *TemplateData, error) {
   805  	s.metrics.ExplorerViews.With(common.Labels{"action": "sendtx"}).Inc()
   806  	data := s.newTemplateData()
   807  	if r.Method == http.MethodPost {
   808  		err := r.ParseForm()
   809  		if err != nil {
   810  			return sendTransactionTpl, data, err
   811  		}
   812  		hex := r.FormValue("hex")
   813  		if len(hex) > 0 {
   814  			res, err := s.chain.SendRawTransaction(hex)
   815  			if err != nil {
   816  				data.SendTxHex = hex
   817  				data.Error = &api.APIError{Text: err.Error(), Public: true}
   818  				return sendTransactionTpl, data, nil
   819  			}
   820  			data.Status = "Transaction sent, result " + res
   821  		}
   822  	}
   823  	return sendTransactionTpl, data, nil
   824  }
   825  
   826  func (s *PublicServer) explorerMempool(w http.ResponseWriter, r *http.Request) (tpl, *TemplateData, error) {
   827  	var mempoolTxids *api.MempoolTxids
   828  	var err error
   829  	s.metrics.ExplorerViews.With(common.Labels{"action": "mempool"}).Inc()
   830  	page, ec := strconv.Atoi(r.URL.Query().Get("page"))
   831  	if ec != nil {
   832  		page = 0
   833  	}
   834  	mempoolTxids, err = s.api.GetMempool(page, mempoolTxsOnPage)
   835  	if err != nil {
   836  		return errorTpl, nil, err
   837  	}
   838  	data := s.newTemplateData()
   839  	data.MempoolTxids = mempoolTxids
   840  	data.Page = mempoolTxids.Page
   841  	data.PagingRange, data.PrevPage, data.NextPage = getPagingRange(mempoolTxids.Page, mempoolTxids.TotalPages)
   842  	return mempoolTpl, data, nil
   843  }
   844  
   845  func getPagingRange(page int, total int) ([]int, int, int) {
   846  	// total==-1 means total is unknown, show only prev/next buttons
   847  	if total >= 0 && total < 2 {
   848  		return nil, 0, 0
   849  	}
   850  	var r []int
   851  	pp, np := page-1, page+1
   852  	if pp < 1 {
   853  		pp = 1
   854  	}
   855  	if total > 0 {
   856  		if np > total {
   857  			np = total
   858  		}
   859  		r = make([]int, 0, 8)
   860  		if total < 6 {
   861  			for i := 1; i <= total; i++ {
   862  				r = append(r, i)
   863  			}
   864  		} else {
   865  			r = append(r, 1)
   866  			if page > 3 {
   867  				r = append(r, 0)
   868  			}
   869  			if pp == 1 {
   870  				if page == 1 {
   871  					r = append(r, np)
   872  					r = append(r, np+1)
   873  					r = append(r, np+2)
   874  				} else {
   875  					r = append(r, page)
   876  					r = append(r, np)
   877  					r = append(r, np+1)
   878  				}
   879  			} else if np == total {
   880  				if page == total {
   881  					r = append(r, pp-2)
   882  					r = append(r, pp-1)
   883  					r = append(r, pp)
   884  				} else {
   885  					r = append(r, pp-1)
   886  					r = append(r, pp)
   887  					r = append(r, page)
   888  				}
   889  			} else {
   890  				r = append(r, pp)
   891  				r = append(r, page)
   892  				r = append(r, np)
   893  			}
   894  			if page <= total-3 {
   895  				r = append(r, 0)
   896  			}
   897  			r = append(r, total)
   898  		}
   899  	}
   900  	return r, pp, np
   901  }
   902  
   903  func (s *PublicServer) apiIndex(r *http.Request, apiVersion int) (interface{}, error) {
   904  	s.metrics.ExplorerViews.With(common.Labels{"action": "api-index"}).Inc()
   905  	return s.api.GetSystemInfo(false)
   906  }
   907  
   908  func (s *PublicServer) apiBlockIndex(r *http.Request, apiVersion int) (interface{}, error) {
   909  	type resBlockIndex struct {
   910  		BlockHash string `json:"blockHash"`
   911  	}
   912  	var err error
   913  	var hash string
   914  	height := -1
   915  	if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 {
   916  		if h, err := strconv.Atoi(r.URL.Path[i+1:]); err == nil {
   917  			height = h
   918  		}
   919  	}
   920  	if height >= 0 {
   921  		hash, err = s.db.GetBlockHash(uint32(height))
   922  	} else {
   923  		_, hash, err = s.db.GetBestBlock()
   924  	}
   925  	if err != nil {
   926  		glog.Error(err)
   927  		return nil, err
   928  	}
   929  	return resBlockIndex{
   930  		BlockHash: hash,
   931  	}, nil
   932  }
   933  
   934  func (s *PublicServer) apiTx(r *http.Request, apiVersion int) (interface{}, error) {
   935  	var txid string
   936  	i := strings.LastIndexByte(r.URL.Path, '/')
   937  	if i > 0 {
   938  		txid = r.URL.Path[i+1:]
   939  	}
   940  	if len(txid) == 0 {
   941  		return nil, api.NewAPIError("Missing txid", true)
   942  	}
   943  	var tx *api.Tx
   944  	var err error
   945  	s.metrics.ExplorerViews.With(common.Labels{"action": "api-tx"}).Inc()
   946  	spendingTxs := false
   947  	p := r.URL.Query().Get("spending")
   948  	if len(p) > 0 {
   949  		spendingTxs, err = strconv.ParseBool(p)
   950  		if err != nil {
   951  			return nil, api.NewAPIError("Parameter 'spending' cannot be converted to boolean", true)
   952  		}
   953  	}
   954  	tx, err = s.api.GetTransaction(txid, spendingTxs, false)
   955  	if err == nil && apiVersion == apiV1 {
   956  		return s.api.TxToV1(tx), nil
   957  	}
   958  	return tx, err
   959  }
   960  
   961  func (s *PublicServer) apiTxSpecific(r *http.Request, apiVersion int) (interface{}, error) {
   962  	var txid string
   963  	i := strings.LastIndexByte(r.URL.Path, '/')
   964  	if i > 0 {
   965  		txid = r.URL.Path[i+1:]
   966  	}
   967  	if len(txid) == 0 {
   968  		return nil, api.NewAPIError("Missing txid", true)
   969  	}
   970  	var tx json.RawMessage
   971  	var err error
   972  	s.metrics.ExplorerViews.With(common.Labels{"action": "api-tx-specific"}).Inc()
   973  	tx, err = s.chain.GetTransactionSpecific(&bchain.Tx{Txid: txid})
   974  	return tx, err
   975  }
   976  
   977  func (s *PublicServer) apiAddress(r *http.Request, apiVersion int) (interface{}, error) {
   978  	var addressParam string
   979  	i := strings.LastIndexByte(r.URL.Path, '/')
   980  	if i > 0 {
   981  		addressParam = r.URL.Path[i+1:]
   982  	}
   983  	if len(addressParam) == 0 {
   984  		return nil, api.NewAPIError("Missing address", true)
   985  	}
   986  	var address *api.Address
   987  	var err error
   988  	s.metrics.ExplorerViews.With(common.Labels{"action": "api-address"}).Inc()
   989  	page, pageSize, details, filter, _, _ := s.getAddressQueryParams(r, api.AccountDetailsTxidHistory, txsInAPI)
   990  	address, err = s.api.GetAddress(addressParam, page, pageSize, details, filter)
   991  	if err == nil && apiVersion == apiV1 {
   992  		return s.api.AddressToV1(address), nil
   993  	}
   994  	return address, err
   995  }
   996  
   997  func (s *PublicServer) apiXpub(r *http.Request, apiVersion int) (interface{}, error) {
   998  	var xpub string
   999  	i := strings.LastIndexByte(r.URL.Path, '/')
  1000  	if i > 0 {
  1001  		xpub = r.URL.Path[i+1:]
  1002  	}
  1003  	if len(xpub) == 0 {
  1004  		return nil, api.NewAPIError("Missing xpub", true)
  1005  	}
  1006  	var address *api.Address
  1007  	var err error
  1008  	s.metrics.ExplorerViews.With(common.Labels{"action": "api-xpub"}).Inc()
  1009  	page, pageSize, details, filter, _, gap := s.getAddressQueryParams(r, api.AccountDetailsTxidHistory, txsInAPI)
  1010  	address, err = s.api.GetXpubAddress(xpub, page, pageSize, details, filter, gap)
  1011  	if err == nil && apiVersion == apiV1 {
  1012  		return s.api.AddressToV1(address), nil
  1013  	}
  1014  	if err == api.ErrUnsupportedXpub {
  1015  		err = api.NewAPIError("XPUB functionality is not supported", true)
  1016  	}
  1017  	return address, err
  1018  }
  1019  
  1020  func (s *PublicServer) apiUtxo(r *http.Request, apiVersion int) (interface{}, error) {
  1021  	var utxo []api.Utxo
  1022  	var err error
  1023  	if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 {
  1024  		onlyConfirmed := false
  1025  		c := r.URL.Query().Get("confirmed")
  1026  		if len(c) > 0 {
  1027  			onlyConfirmed, err = strconv.ParseBool(c)
  1028  			if err != nil {
  1029  				return nil, api.NewAPIError("Parameter 'confirmed' cannot be converted to boolean", true)
  1030  			}
  1031  		}
  1032  		gap, ec := strconv.Atoi(r.URL.Query().Get("gap"))
  1033  		if ec != nil {
  1034  			gap = 0
  1035  		}
  1036  		utxo, err = s.api.GetXpubUtxo(r.URL.Path[i+1:], onlyConfirmed, gap)
  1037  		if err == nil {
  1038  			s.metrics.ExplorerViews.With(common.Labels{"action": "api-xpub-utxo"}).Inc()
  1039  		} else {
  1040  			utxo, err = s.api.GetAddressUtxo(r.URL.Path[i+1:], onlyConfirmed)
  1041  			s.metrics.ExplorerViews.With(common.Labels{"action": "api-address-utxo"}).Inc()
  1042  		}
  1043  		if err == nil && apiVersion == apiV1 {
  1044  			return s.api.AddressUtxoToV1(utxo), nil
  1045  		}
  1046  	}
  1047  	return utxo, err
  1048  }
  1049  
  1050  func (s *PublicServer) apiBalanceHistory(r *http.Request, apiVersion int) (interface{}, error) {
  1051  	var history []api.BalanceHistory
  1052  	var fromTimestamp, toTimestamp int64
  1053  	var err error
  1054  	if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 {
  1055  		gap, ec := strconv.Atoi(r.URL.Query().Get("gap"))
  1056  		if ec != nil {
  1057  			gap = 0
  1058  		}
  1059  		from := r.URL.Query().Get("from")
  1060  		if from != "" {
  1061  			fromTimestamp, err = strconv.ParseInt(from, 10, 64)
  1062  			if err != nil {
  1063  				return history, err
  1064  			}
  1065  		}
  1066  		to := r.URL.Query().Get("to")
  1067  		if to != "" {
  1068  			toTimestamp, err = strconv.ParseInt(to, 10, 64)
  1069  			if err != nil {
  1070  				return history, err
  1071  			}
  1072  		}
  1073  		var groupBy uint64
  1074  		groupBy, err = strconv.ParseUint(r.URL.Query().Get("groupBy"), 10, 32)
  1075  		if err != nil || groupBy == 0 {
  1076  			groupBy = 3600
  1077  		}
  1078  		fiat := r.URL.Query().Get("fiatcurrency")
  1079  		var fiatArray []string
  1080  		if fiat != "" {
  1081  			fiatArray = []string{fiat}
  1082  		}
  1083  		history, err = s.api.GetXpubBalanceHistory(r.URL.Path[i+1:], fromTimestamp, toTimestamp, fiatArray, gap, uint32(groupBy))
  1084  		if err == nil {
  1085  			s.metrics.ExplorerViews.With(common.Labels{"action": "api-xpub-balancehistory"}).Inc()
  1086  		} else {
  1087  			history, err = s.api.GetBalanceHistory(r.URL.Path[i+1:], fromTimestamp, toTimestamp, fiatArray, uint32(groupBy))
  1088  			s.metrics.ExplorerViews.With(common.Labels{"action": "api-address-balancehistory"}).Inc()
  1089  		}
  1090  	}
  1091  	return history, err
  1092  }
  1093  
  1094  func (s *PublicServer) apiBlock(r *http.Request, apiVersion int) (interface{}, error) {
  1095  	var block *api.Block
  1096  	var err error
  1097  	s.metrics.ExplorerViews.With(common.Labels{"action": "api-block"}).Inc()
  1098  	if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 {
  1099  		page, ec := strconv.Atoi(r.URL.Query().Get("page"))
  1100  		if ec != nil {
  1101  			page = 0
  1102  		}
  1103  		block, err = s.api.GetBlock(r.URL.Path[i+1:], page, txsInAPI)
  1104  		if err == nil && apiVersion == apiV1 {
  1105  			return s.api.BlockToV1(block), nil
  1106  		}
  1107  	}
  1108  	return block, err
  1109  }
  1110  
  1111  func (s *PublicServer) apiFeeStats(r *http.Request, apiVersion int) (interface{}, error) {
  1112  	var feeStats *api.FeeStats
  1113  	var err error
  1114  	s.metrics.ExplorerViews.With(common.Labels{"action": "api-feestats"}).Inc()
  1115  	if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 {
  1116  		feeStats, err = s.api.GetFeeStats(r.URL.Path[i+1:])
  1117  	}
  1118  	return feeStats, err
  1119  }
  1120  
  1121  type resultSendTransaction struct {
  1122  	Result string `json:"result"`
  1123  }
  1124  
  1125  func (s *PublicServer) apiSendTx(r *http.Request, apiVersion int) (interface{}, error) {
  1126  	var err error
  1127  	var res resultSendTransaction
  1128  	var hex string
  1129  	s.metrics.ExplorerViews.With(common.Labels{"action": "api-sendtx"}).Inc()
  1130  	if r.Method == http.MethodPost {
  1131  		data, err := ioutil.ReadAll(r.Body)
  1132  		if err != nil {
  1133  			return nil, api.NewAPIError("Missing tx blob", true)
  1134  		}
  1135  		hex = string(data)
  1136  	} else {
  1137  		if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 {
  1138  			hex = r.URL.Path[i+1:]
  1139  		}
  1140  	}
  1141  	if len(hex) > 0 {
  1142  		res.Result, err = s.chain.SendRawTransaction(hex)
  1143  		if err != nil {
  1144  			return nil, api.NewAPIError(err.Error(), true)
  1145  		}
  1146  		return res, nil
  1147  	}
  1148  	return nil, api.NewAPIError("Missing tx blob", true)
  1149  }
  1150  
  1151  // apiTickersList returns a list of available FiatRates currencies
  1152  func (s *PublicServer) apiTickersList(r *http.Request, apiVersion int) (interface{}, error) {
  1153  	s.metrics.ExplorerViews.With(common.Labels{"action": "api-tickers-list"}).Inc()
  1154  	timestampString := strings.ToLower(r.URL.Query().Get("timestamp"))
  1155  	timestamp, err := strconv.ParseInt(timestampString, 10, 64)
  1156  	if err != nil {
  1157  		return nil, api.NewAPIError("Parameter \"timestamp\" is not a valid Unix timestamp.", true)
  1158  	}
  1159  	result, err := s.api.GetFiatRatesTickersList(timestamp)
  1160  	return result, err
  1161  }
  1162  
  1163  // apiTickers returns FiatRates ticker prices for the specified block or timestamp.
  1164  func (s *PublicServer) apiTickers(r *http.Request, apiVersion int) (interface{}, error) {
  1165  	var result *db.ResultTickerAsString
  1166  	var err error
  1167  
  1168  	currency := strings.ToLower(r.URL.Query().Get("currency"))
  1169  	var currencies []string
  1170  	if currency != "" {
  1171  		currencies = []string{currency}
  1172  	}
  1173  
  1174  	if block := r.URL.Query().Get("block"); block != "" {
  1175  		// Get tickers for specified block height or block hash
  1176  		s.metrics.ExplorerViews.With(common.Labels{"action": "api-tickers-block"}).Inc()
  1177  		result, err = s.api.GetFiatRatesForBlockID(block, currencies)
  1178  	} else if timestampString := r.URL.Query().Get("timestamp"); timestampString != "" {
  1179  		// Get tickers for specified timestamp
  1180  		s.metrics.ExplorerViews.With(common.Labels{"action": "api-tickers-date"}).Inc()
  1181  
  1182  		timestamp, err := strconv.ParseInt(timestampString, 10, 64)
  1183  		if err != nil {
  1184  			return nil, api.NewAPIError("Parameter \"timestamp\" is not a valid Unix timestamp.", true)
  1185  		}
  1186  
  1187  		resultTickers, err := s.api.GetFiatRatesForTimestamps([]int64{timestamp}, currencies)
  1188  		if err != nil {
  1189  			return nil, err
  1190  		}
  1191  		result = &resultTickers.Tickers[0]
  1192  	} else {
  1193  		// No parameters - get the latest available ticker
  1194  		s.metrics.ExplorerViews.With(common.Labels{"action": "api-tickers-last"}).Inc()
  1195  		result, err = s.api.GetCurrentFiatRates(currencies)
  1196  	}
  1197  	if err != nil {
  1198  		return nil, err
  1199  	}
  1200  	return result, nil
  1201  }
  1202  
  1203  type resultEstimateFeeAsString struct {
  1204  	Result string `json:"result"`
  1205  }
  1206  
  1207  func (s *PublicServer) apiEstimateFee(r *http.Request, apiVersion int) (interface{}, error) {
  1208  	var res resultEstimateFeeAsString
  1209  	s.metrics.ExplorerViews.With(common.Labels{"action": "api-estimatefee"}).Inc()
  1210  	if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 {
  1211  		b := r.URL.Path[i+1:]
  1212  		if len(b) > 0 {
  1213  			blocks, err := strconv.Atoi(b)
  1214  			if err != nil {
  1215  				return nil, api.NewAPIError("Parameter 'number of blocks' is not a number", true)
  1216  			}
  1217  			conservative := true
  1218  			c := r.URL.Query().Get("conservative")
  1219  			if len(c) > 0 {
  1220  				conservative, err = strconv.ParseBool(c)
  1221  				if err != nil {
  1222  					return nil, api.NewAPIError("Parameter 'conservative' cannot be converted to boolean", true)
  1223  				}
  1224  			}
  1225  			var fee big.Int
  1226  			fee, err = s.chain.EstimateSmartFee(blocks, conservative)
  1227  			if err != nil {
  1228  				fee, err = s.chain.EstimateFee(blocks)
  1229  				if err != nil {
  1230  					return nil, err
  1231  				}
  1232  			}
  1233  			res.Result = s.chainParser.AmountToDecimalString(&fee)
  1234  			return res, nil
  1235  		}
  1236  	}
  1237  	return nil, api.NewAPIError("Missing parameter 'number of blocks'", true)
  1238  }