github.com/cryptohub-digital/blockbook-fork@v0.0.0-20230713133354-673c927af7f1/server/public.go (about)

     1  package server
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"fmt"
     7  	"html/template"
     8  	"io"
     9  	"math/big"
    10  	"net/http"
    11  	"net/url"
    12  	"os"
    13  	"path/filepath"
    14  	"reflect"
    15  	"regexp"
    16  	"runtime"
    17  	"runtime/debug"
    18  	"sort"
    19  	"strconv"
    20  	"strings"
    21  	"time"
    22  
    23  	"github.com/cryptohub-digital/blockbook-fork/api"
    24  	"github.com/cryptohub-digital/blockbook-fork/bchain"
    25  	"github.com/cryptohub-digital/blockbook-fork/common"
    26  	"github.com/cryptohub-digital/blockbook-fork/db"
    27  	"github.com/cryptohub-digital/blockbook-fork/fiat"
    28  	"github.com/golang/glog"
    29  )
    30  
    31  const txsOnPage = 25
    32  const blocksOnPage = 50
    33  const mempoolTxsOnPage = 50
    34  const txsInAPI = 1000
    35  
    36  const secondaryCoinCookieName = "secondary_coin"
    37  
    38  const (
    39  	_ = iota
    40  	apiV1
    41  	apiV2
    42  )
    43  
    44  // PublicServer provides public http server functionality
    45  type PublicServer struct {
    46  	htmlTemplates[TemplateData]
    47  	binding             string
    48  	certFiles           string
    49  	socketio            *SocketIoServer
    50  	websocket           *WebsocketServer
    51  	https               *http.Server
    52  	db                  *db.RocksDB
    53  	txCache             *db.TxCache
    54  	chain               bchain.BlockChain
    55  	chainParser         bchain.BlockChainParser
    56  	mempool             bchain.Mempool
    57  	api                 *api.Worker
    58  	explorerURL         string
    59  	internalExplorer    bool
    60  	is                  *common.InternalState
    61  	fiatRates           *fiat.FiatRates
    62  	useSatsAmountFormat bool
    63  }
    64  
    65  // NewPublicServer creates new public server http interface to blockbook and returns its handle
    66  // only basic functionality is mapped, to map all functions, call
    67  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, fiatRates *fiat.FiatRates, debugMode bool) (*PublicServer, error) {
    68  
    69  	api, err := api.NewWorker(db, chain, mempool, txCache, metrics, is, fiatRates)
    70  	if err != nil {
    71  		return nil, err
    72  	}
    73  
    74  	socketio, err := NewSocketIoServer(db, chain, mempool, txCache, metrics, is, fiatRates)
    75  	if err != nil {
    76  		return nil, err
    77  	}
    78  
    79  	websocket, err := NewWebsocketServer(db, chain, mempool, txCache, metrics, is, fiatRates)
    80  	if err != nil {
    81  		return nil, err
    82  	}
    83  
    84  	addr, path := splitBinding(binding)
    85  	serveMux := http.NewServeMux()
    86  	https := &http.Server{
    87  		Addr:    addr,
    88  		Handler: serveMux,
    89  	}
    90  
    91  	s := &PublicServer{
    92  		htmlTemplates: htmlTemplates[TemplateData]{
    93  			metrics: metrics,
    94  			debug:   debugMode,
    95  		},
    96  		binding:             binding,
    97  		certFiles:           certFiles,
    98  		https:               https,
    99  		api:                 api,
   100  		socketio:            socketio,
   101  		websocket:           websocket,
   102  		db:                  db,
   103  		txCache:             txCache,
   104  		chain:               chain,
   105  		chainParser:         chain.GetChainParser(),
   106  		mempool:             mempool,
   107  		explorerURL:         explorerURL,
   108  		internalExplorer:    explorerURL == "",
   109  		is:                  is,
   110  		fiatRates:           fiatRates,
   111  		useSatsAmountFormat: chain.GetChainParser().GetChainType() == bchain.ChainBitcoinType && chain.GetChainParser().AmountDecimals() == 8,
   112  	}
   113  	s.htmlTemplates.newTemplateData = s.newTemplateData
   114  	s.htmlTemplates.newTemplateDataWithError = s.newTemplateDataWithError
   115  	s.htmlTemplates.parseTemplates = s.parseTemplates
   116  	s.htmlTemplates.postHtmlTemplateHandler = s.postHtmlTemplateHandler
   117  	s.templates = s.parseTemplates()
   118  
   119  	// map only basic functions, the rest is enabled by method MapFullPublicInterface
   120  	serveMux.Handle(path+"favicon.ico", http.FileServer(http.Dir("./static/")))
   121  	serveMux.Handle(path+"static/", http.StripPrefix("/static/", http.FileServer(http.Dir("./static/"))))
   122  	// default handler
   123  	serveMux.HandleFunc(path, s.htmlTemplateHandler(s.explorerIndex))
   124  	// default API handler
   125  	serveMux.HandleFunc(path+"api/", s.jsonHandler(s.apiIndex, apiV2))
   126  
   127  	return s, nil
   128  }
   129  
   130  // Run starts the server
   131  func (s *PublicServer) Run() error {
   132  	if s.certFiles == "" {
   133  		glog.Info("public server: starting to listen on http://", s.https.Addr)
   134  		return s.https.ListenAndServe()
   135  	}
   136  	glog.Info("public server starting to listen on https://", s.https.Addr)
   137  	return s.https.ListenAndServeTLS(fmt.Sprint(s.certFiles, ".crt"), fmt.Sprint(s.certFiles, ".key"))
   138  }
   139  
   140  // ConnectFullPublicInterface enables complete public functionality
   141  func (s *PublicServer) ConnectFullPublicInterface() {
   142  	serveMux := s.https.Handler.(*http.ServeMux)
   143  	_, path := splitBinding(s.binding)
   144  	// support for test pages
   145  	serveMux.Handle(path+"test-socketio.html", http.FileServer(http.Dir("./static/")))
   146  	serveMux.Handle(path+"test-websocket.html", http.FileServer(http.Dir("./static/")))
   147  	if s.internalExplorer {
   148  		// internal explorer handlers
   149  		serveMux.HandleFunc(path+"tx/", s.htmlTemplateHandler(s.explorerTx))
   150  		serveMux.HandleFunc(path+"address/", s.htmlTemplateHandler(s.explorerAddress))
   151  		serveMux.HandleFunc(path+"xpub/", s.htmlTemplateHandler(s.explorerXpub))
   152  		serveMux.HandleFunc(path+"search/", s.htmlTemplateHandler(s.explorerSearch))
   153  		serveMux.HandleFunc(path+"blocks", s.htmlTemplateHandler(s.explorerBlocks))
   154  		serveMux.HandleFunc(path+"block/", s.htmlTemplateHandler(s.explorerBlock))
   155  		serveMux.HandleFunc(path+"spending/", s.htmlTemplateHandler(s.explorerSpendingTx))
   156  		serveMux.HandleFunc(path+"sendtx", s.htmlTemplateHandler(s.explorerSendTx))
   157  		serveMux.HandleFunc(path+"mempool", s.htmlTemplateHandler(s.explorerMempool))
   158  		if s.chainParser.GetChainType() == bchain.ChainEthereumType {
   159  			serveMux.HandleFunc(path+"nft/", s.htmlTemplateHandler(s.explorerNftDetail))
   160  		}
   161  	} else {
   162  		// redirect to wallet requests for tx and address, possibly to external site
   163  		serveMux.HandleFunc(path+"tx/", s.txRedirect)
   164  		serveMux.HandleFunc(path+"address/", s.addressRedirect)
   165  	}
   166  	// API calls
   167  	// default api without version can be changed to different version at any time
   168  	// use versioned api for stability
   169  
   170  	var apiDefault int
   171  	// ethereum supports only api V2
   172  	if s.chainParser.GetChainType() == bchain.ChainEthereumType || s.chainParser.GetChainType() == bchain.ChainCoreCoinType {
   173  		apiDefault = apiV2
   174  	} else {
   175  		apiDefault = apiV1
   176  		// legacy v1 format
   177  		serveMux.HandleFunc(path+"api/v1/block-index/", s.jsonHandler(s.apiBlockIndex, apiV1))
   178  		serveMux.HandleFunc(path+"api/v1/tx-specific/", s.jsonHandler(s.apiTxSpecific, apiV1))
   179  		serveMux.HandleFunc(path+"api/v1/tx/", s.jsonHandler(s.apiTx, apiV1))
   180  		serveMux.HandleFunc(path+"api/v1/address/", s.jsonHandler(s.apiAddress, apiV1))
   181  		serveMux.HandleFunc(path+"api/v1/utxo/", s.jsonHandler(s.apiUtxo, apiV1))
   182  		serveMux.HandleFunc(path+"api/v1/block/", s.jsonHandler(s.apiBlock, apiV1))
   183  		serveMux.HandleFunc(path+"api/v1/sendtx/", s.jsonHandler(s.apiSendTx, apiV1))
   184  		serveMux.HandleFunc(path+"api/v1/estimatefee/", s.jsonHandler(s.apiEstimateFee, apiV1))
   185  	}
   186  	serveMux.HandleFunc(path+"api/block-index/", s.jsonHandler(s.apiBlockIndex, apiDefault))
   187  	serveMux.HandleFunc(path+"api/tx-specific/", s.jsonHandler(s.apiTxSpecific, apiDefault))
   188  	serveMux.HandleFunc(path+"api/tx/", s.jsonHandler(s.apiTx, apiDefault))
   189  	serveMux.HandleFunc(path+"api/address/", s.jsonHandler(s.apiAddress, apiDefault))
   190  	serveMux.HandleFunc(path+"api/xpub/", s.jsonHandler(s.apiXpub, apiDefault))
   191  	serveMux.HandleFunc(path+"api/utxo/", s.jsonHandler(s.apiUtxo, apiDefault))
   192  	serveMux.HandleFunc(path+"api/block/", s.jsonHandler(s.apiBlock, apiDefault))
   193  	serveMux.HandleFunc(path+"api/rawblock/", s.jsonHandler(s.apiBlockRaw, apiDefault))
   194  	serveMux.HandleFunc(path+"api/sendtx/", s.jsonHandler(s.apiSendTx, apiDefault))
   195  	serveMux.HandleFunc(path+"api/estimatefee/", s.jsonHandler(s.apiEstimateFee, apiDefault))
   196  	serveMux.HandleFunc(path+"api/balancehistory/", s.jsonHandler(s.apiBalanceHistory, apiDefault))
   197  	// v2 format
   198  	serveMux.HandleFunc(path+"api/v2/block-index/", s.jsonHandler(s.apiBlockIndex, apiV2))
   199  	serveMux.HandleFunc(path+"api/v2/tx-specific/", s.jsonHandler(s.apiTxSpecific, apiV2))
   200  	serveMux.HandleFunc(path+"api/v2/tx/", s.jsonHandler(s.apiTx, apiV2))
   201  	serveMux.HandleFunc(path+"api/v2/address/", s.jsonHandler(s.apiAddress, apiV2))
   202  	serveMux.HandleFunc(path+"api/v2/xpub/", s.jsonHandler(s.apiXpub, apiV2))
   203  	serveMux.HandleFunc(path+"api/v2/utxo/", s.jsonHandler(s.apiUtxo, apiV2))
   204  	serveMux.HandleFunc(path+"api/v2/block/", s.jsonHandler(s.apiBlock, apiV2))
   205  	serveMux.HandleFunc(path+"api/v2/rawblock/", s.jsonHandler(s.apiBlockRaw, apiDefault))
   206  	serveMux.HandleFunc(path+"api/v2/sendtx/", s.jsonHandler(s.apiSendTx, apiV2))
   207  	serveMux.HandleFunc(path+"api/v2/estimatefee/", s.jsonHandler(s.apiEstimateFee, apiV2))
   208  	serveMux.HandleFunc(path+"api/v2/feestats/", s.jsonHandler(s.apiFeeStats, apiV2))
   209  	serveMux.HandleFunc(path+"api/v2/balancehistory/", s.jsonHandler(s.apiBalanceHistory, apiDefault))
   210  	serveMux.HandleFunc(path+"api/v2/tickers/", s.jsonHandler(s.apiTickers, apiV2))
   211  	serveMux.HandleFunc(path+"api/v2/multi-tickers/", s.jsonHandler(s.apiMultiTickers, apiV2))
   212  	serveMux.HandleFunc(path+"api/v2/tickers-list/", s.jsonHandler(s.apiAvailableVsCurrencies, apiV2))
   213  	// socket.io interface
   214  	serveMux.Handle(path+"socket.io/", s.socketio.GetHandler())
   215  	// websocket interface
   216  	serveMux.Handle(path+"websocket", s.websocket.GetHandler())
   217  }
   218  
   219  // Close closes the server
   220  func (s *PublicServer) Close() error {
   221  	glog.Infof("public server: closing")
   222  	return s.https.Close()
   223  }
   224  
   225  // Shutdown shuts down the server
   226  func (s *PublicServer) Shutdown(ctx context.Context) error {
   227  	glog.Infof("public server: shutdown")
   228  	return s.https.Shutdown(ctx)
   229  }
   230  
   231  // OnNewBlock notifies users subscribed to bitcoind/hashblock about new block
   232  func (s *PublicServer) OnNewBlock(hash string, height uint32) {
   233  	s.socketio.OnNewBlockHash(hash)
   234  	s.websocket.OnNewBlock(hash, height)
   235  }
   236  
   237  // OnNewFiatRatesTicker notifies users subscribed to bitcoind/fiatrates about new ticker
   238  func (s *PublicServer) OnNewFiatRatesTicker(ticker *common.CurrencyRatesTicker) {
   239  	s.websocket.OnNewFiatRatesTicker(ticker)
   240  }
   241  
   242  // OnNewTxAddr notifies users subscribed to notification about new tx
   243  func (s *PublicServer) OnNewTxAddr(tx *bchain.Tx, desc bchain.AddressDescriptor) {
   244  	s.socketio.OnNewTxAddr(tx.Txid, desc)
   245  }
   246  
   247  // OnNewTx notifies users subscribed to notification about new tx
   248  func (s *PublicServer) OnNewTx(tx *bchain.MempoolTx) {
   249  	s.websocket.OnNewTx(tx)
   250  }
   251  
   252  func (s *PublicServer) txRedirect(w http.ResponseWriter, r *http.Request) {
   253  	http.Redirect(w, r, joinURL(s.explorerURL, r.URL.Path), http.StatusFound)
   254  	s.metrics.ExplorerViews.With(common.Labels{"action": "tx-redirect"}).Inc()
   255  }
   256  
   257  func (s *PublicServer) addressRedirect(w http.ResponseWriter, r *http.Request) {
   258  	http.Redirect(w, r, joinURL(s.explorerURL, r.URL.Path), http.StatusFound)
   259  	s.metrics.ExplorerViews.With(common.Labels{"action": "address-redirect"}).Inc()
   260  }
   261  
   262  func splitBinding(binding string) (addr string, path string) {
   263  	i := strings.Index(binding, "/")
   264  	if i >= 0 {
   265  		return binding[0:i], binding[i:]
   266  	}
   267  	return binding, "/"
   268  }
   269  
   270  func joinURL(base string, part string) string {
   271  	if len(base) > 0 {
   272  		if len(base) > 0 && base[len(base)-1] == '/' && len(part) > 0 && part[0] == '/' {
   273  			return base + part[1:]
   274  		}
   275  		return base + part
   276  	}
   277  	return part
   278  }
   279  
   280  func getFunctionName(i interface{}) string {
   281  	name := runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name()
   282  	start := strings.LastIndex(name, ".")
   283  	end := strings.LastIndex(name, "-")
   284  	if start > 0 && end > start {
   285  		name = name[start+1 : end]
   286  	}
   287  	return name
   288  }
   289  
   290  func (s *PublicServer) jsonHandler(handler func(r *http.Request, apiVersion int) (interface{}, error), apiVersion int) func(w http.ResponseWriter, r *http.Request) {
   291  	type jsonError struct {
   292  		Text       string `json:"error"`
   293  		HTTPStatus int    `json:"-"`
   294  	}
   295  	handlerName := getFunctionName(handler)
   296  	return func(w http.ResponseWriter, r *http.Request) {
   297  		var data interface{}
   298  		var err error
   299  		defer func() {
   300  			if e := recover(); e != nil {
   301  				glog.Error(handlerName, " recovered from panic: ", e)
   302  				debug.PrintStack()
   303  				if s.debug {
   304  					data = jsonError{fmt.Sprint("Internal server error: recovered from panic ", e), http.StatusInternalServerError}
   305  				} else {
   306  					data = jsonError{"Internal server error", http.StatusInternalServerError}
   307  				}
   308  			}
   309  			w.Header().Set("Content-Type", "application/json; charset=utf-8")
   310  			if e, isError := data.(jsonError); isError {
   311  				w.WriteHeader(e.HTTPStatus)
   312  			}
   313  			err = json.NewEncoder(w).Encode(data)
   314  			if err != nil {
   315  				glog.Warning("json encode ", err)
   316  			}
   317  			s.metrics.ExplorerPendingRequests.With((common.Labels{"method": handlerName})).Dec()
   318  		}()
   319  		s.metrics.ExplorerPendingRequests.With((common.Labels{"method": handlerName})).Inc()
   320  		data, err = handler(r, apiVersion)
   321  		if err != nil || data == nil {
   322  			if apiErr, ok := err.(*api.APIError); ok {
   323  				if apiErr.Public {
   324  					data = jsonError{apiErr.Error(), http.StatusBadRequest}
   325  				} else {
   326  					data = jsonError{apiErr.Error(), http.StatusInternalServerError}
   327  				}
   328  			} else {
   329  				if err != nil {
   330  					glog.Error(handlerName, " error: ", err)
   331  				}
   332  				if s.debug {
   333  					if data != nil {
   334  						data = jsonError{fmt.Sprintf("Internal server error: %v, data %+v", err, data), http.StatusInternalServerError}
   335  					} else {
   336  						data = jsonError{fmt.Sprintf("Internal server error: %v", err), http.StatusInternalServerError}
   337  					}
   338  				} else {
   339  					data = jsonError{"Internal server error", http.StatusInternalServerError}
   340  				}
   341  			}
   342  		}
   343  	}
   344  }
   345  
   346  func (s *PublicServer) newTemplateData(r *http.Request) *TemplateData {
   347  	t := &TemplateData{
   348  		CoinName:         s.is.Coin,
   349  		CoinShortcut:     s.is.CoinShortcut,
   350  		CoinLabel:        s.is.CoinLabel,
   351  		ChainType:        s.chainParser.GetChainType(),
   352  		InternalExplorer: s.internalExplorer && !s.is.InitialSync,
   353  		TOSLink:          api.Text.TOSLink,
   354  	}
   355  	if t.ChainType == bchain.ChainEthereumType || t.ChainType == bchain.ChainCoreCoinType {
   356  		t.FungibleTokenName = bchain.TokenTypeMap[bchain.FungibleToken]
   357  		t.NonFungibleTokenName = bchain.TokenTypeMap[bchain.NonFungibleToken]
   358  		t.MultiTokenName = bchain.TokenTypeMap[bchain.MultiToken]
   359  	}
   360  	if !s.debug {
   361  		t.Minified = ".min.3"
   362  	}
   363  	if s.is.HasFiatRates {
   364  		// get the secondary coin and if it should be shown either from query parameters "secondary" and "use_secondary"
   365  		// or from the cookie "secondary_coin" in the format secondary=use_secondary, for example EUR=true
   366  		// the query parameters take precedence over the cookie
   367  		var cookieSecondary string
   368  		var cookieUseSecondary bool
   369  		cookie, _ := r.Cookie(secondaryCoinCookieName)
   370  		if cookie != nil {
   371  			a := strings.Split(cookie.Value, "=")
   372  			if len(a) == 2 {
   373  				cookieSecondary = a[0]
   374  				cookieUseSecondary, _ = strconv.ParseBool(a[1])
   375  			}
   376  		}
   377  		secondary := strings.ToLower(r.URL.Query().Get("secondary"))
   378  		if secondary == "" {
   379  			if cookieSecondary != "" {
   380  				secondary = strings.ToLower(cookieSecondary)
   381  			} else {
   382  				secondary = "usd"
   383  			}
   384  		}
   385  		ticker := s.fiatRates.GetCurrentTicker(secondary, "")
   386  		if ticker == nil && secondary != "usd" {
   387  			secondary = "usd"
   388  			ticker = s.fiatRates.GetCurrentTicker(secondary, "")
   389  		}
   390  		if ticker != nil {
   391  			t.SecondaryCoin = strings.ToUpper(secondary)
   392  			t.CurrentSecondaryCoinRate = float64(ticker.Rates[secondary])
   393  			t.CurrentTicker = ticker
   394  			t.SecondaryCurrencies = make([]string, 0, len(ticker.Rates))
   395  			for k := range ticker.Rates {
   396  				t.SecondaryCurrencies = append(t.SecondaryCurrencies, strings.ToUpper(k))
   397  			}
   398  			sort.Strings(t.SecondaryCurrencies) // sort to get deterministic results
   399  			t.UseSecondaryCoin, _ = strconv.ParseBool(r.URL.Query().Get("use_secondary"))
   400  			if !t.UseSecondaryCoin {
   401  				t.UseSecondaryCoin = cookieUseSecondary
   402  			}
   403  		}
   404  	}
   405  	return t
   406  }
   407  
   408  func (s *PublicServer) newTemplateDataWithError(error *api.APIError, r *http.Request) *TemplateData {
   409  	td := s.newTemplateData(r)
   410  	td.Error = error
   411  	return td
   412  }
   413  
   414  const (
   415  	indexTpl = iota + errorInternalTpl + 1
   416  	txTpl
   417  	addressTpl
   418  	xpubTpl
   419  	blocksTpl
   420  	blockTpl
   421  	sendTransactionTpl
   422  	mempoolTpl
   423  	nftDetailTpl
   424  
   425  	publicTplCount
   426  )
   427  
   428  // TemplateData is used to transfer data to the templates
   429  type TemplateData struct {
   430  	CoinName                 string
   431  	CoinShortcut             string
   432  	CoinLabel                string
   433  	InternalExplorer         bool
   434  	ChainType                bchain.ChainType
   435  	FungibleTokenName        bchain.TokenTypeName
   436  	NonFungibleTokenName     bchain.TokenTypeName
   437  	MultiTokenName           bchain.TokenTypeName
   438  	Address                  *api.Address
   439  	AddrStr                  string
   440  	Tx                       *api.Tx
   441  	Error                    *api.APIError
   442  	Blocks                   *api.Blocks
   443  	Block                    *api.Block
   444  	Info                     *api.SystemInfo
   445  	MempoolTxids             *api.MempoolTxids
   446  	Page                     int
   447  	PrevPage                 int
   448  	NextPage                 int
   449  	PagingRange              []int
   450  	PageParams               template.URL
   451  	Minified                 string
   452  	TOSLink                  string
   453  	SendTxHex                string
   454  	Status                   string
   455  	NonZeroBalanceTokens     bool
   456  	TokenId                  string
   457  	URI                      string
   458  	ContractInfo             *bchain.ContractInfo
   459  	SecondaryCoin            string
   460  	UseSecondaryCoin         bool
   461  	CurrentSecondaryCoinRate float64
   462  	CurrentTicker            *common.CurrencyRatesTicker
   463  	SecondaryCurrencies      []string
   464  	TxDate                   string
   465  	TxSecondaryCoinRate      float64
   466  	TxTicker                 *common.CurrencyRatesTicker
   467  }
   468  
   469  func (s *PublicServer) parseTemplates() []*template.Template {
   470  	templateFuncMap := template.FuncMap{
   471  		"timeSpan":                 timeSpan,
   472  		"relativeTime":             relativeTime,
   473  		"unixTimeSpan":             unixTimeSpan,
   474  		"amountSpan":               s.amountSpan,
   475  		"tokenAmountSpan":          s.tokenAmountSpan,
   476  		"amountSatsSpan":           s.amountSatsSpan,
   477  		"formattedAmountSpan":      s.formattedAmountSpan,
   478  		"summaryValuesSpan":        s.summaryValuesSpan,
   479  		"addressAlias":             addressAlias,
   480  		"addressAliasSpan":         addressAliasSpan,
   481  		"formatAmount":             s.formatAmount,
   482  		"formatAmountWithDecimals": formatAmountWithDecimals,
   483  		"formatInt64":              formatInt64,
   484  		"formatInt":                formatInt,
   485  		"formatUint32":             formatUint32,
   486  		"formatBigInt":             formatBigInt,
   487  		"setTxToTemplateData":      setTxToTemplateData,
   488  		"feePerByte":               feePerByte,
   489  		"isOwnAddress":             isOwnAddress,
   490  		"toJSON":                   toJSON,
   491  		"tokenTransfersCount":      tokenTransfersCount,
   492  		"tokenCount":               tokenCount,
   493  		"hasPrefix":                strings.HasPrefix,
   494  		"jsStr":                    jsStr,
   495  	}
   496  	var createTemplate func(filenames ...string) *template.Template
   497  	if s.debug {
   498  		createTemplate = func(filenames ...string) *template.Template {
   499  			if len(filenames) == 0 {
   500  				panic("Missing templates")
   501  			}
   502  			return template.Must(template.New(filepath.Base(filenames[0])).Funcs(templateFuncMap).ParseFiles(filenames...))
   503  		}
   504  	} else {
   505  		createTemplate = func(filenames ...string) *template.Template {
   506  			if len(filenames) == 0 {
   507  				panic("Missing templates")
   508  			}
   509  			t := template.New(filepath.Base(filenames[0])).Funcs(templateFuncMap)
   510  			for _, filename := range filenames {
   511  				b, err := os.ReadFile(filename)
   512  				if err != nil {
   513  					panic(err)
   514  				}
   515  				// perform very simple minification - replace leading spaces used as formatting and new lines
   516  				r := regexp.MustCompile(`\n\s*`)
   517  				b = r.ReplaceAll(b, []byte{})
   518  				s := string(b)
   519  				name := filepath.Base(filename)
   520  				var tt *template.Template
   521  				if name == t.Name() {
   522  					tt = t
   523  				} else {
   524  					tt = t.New(name)
   525  				}
   526  				_, err = tt.Parse(s)
   527  				if err != nil {
   528  					panic(err)
   529  				}
   530  			}
   531  			return t
   532  		}
   533  	}
   534  	t := make([]*template.Template, publicTplCount)
   535  	t[errorTpl] = createTemplate("./static/templates/error.html", "./static/templates/base.html")
   536  	t[errorInternalTpl] = createTemplate("./static/templates/error.html", "./static/templates/base.html")
   537  	t[indexTpl] = createTemplate("./static/templates/index.html", "./static/templates/base.html")
   538  	t[blocksTpl] = createTemplate("./static/templates/blocks.html", "./static/templates/paging.html", "./static/templates/base.html")
   539  	t[sendTransactionTpl] = createTemplate("./static/templates/sendtx.html", "./static/templates/base.html")
   540  	if s.chainParser.GetChainType() == bchain.ChainEthereumType {
   541  		t[txTpl] = createTemplate("./static/templates/tx.html", "./static/templates/txdetail_ethereumtype.html", "./static/templates/base.html")
   542  		t[addressTpl] = createTemplate("./static/templates/address.html", "./static/templates/txdetail_ethereumtype.html", "./static/templates/paging.html", "./static/templates/base.html")
   543  		t[blockTpl] = createTemplate("./static/templates/block.html", "./static/templates/txdetail_ethereumtype.html", "./static/templates/paging.html", "./static/templates/base.html")
   544  		t[nftDetailTpl] = createTemplate("./static/templates/tokenDetail.html", "./static/templates/base.html")
   545  	} else if s.chainParser.GetChainType() == bchain.ChainCoreCoinType {
   546  		t[txTpl] = createTemplate("./static/templates/tx.html", "./static/templates/txdetail_corecointype.html", "./static/templates/base.html")
   547  		t[addressTpl] = createTemplate("./static/templates/address.html", "./static/templates/txdetail_corecointype.html", "./static/templates/paging.html", "./static/templates/base.html")
   548  		t[blockTpl] = createTemplate("./static/templates/block.html", "./static/templates/txdetail_corecointype.html", "./static/templates/paging.html", "./static/templates/base.html")
   549  	} else {
   550  		t[txTpl] = createTemplate("./static/templates/tx.html", "./static/templates/txdetail.html", "./static/templates/base.html")
   551  		t[addressTpl] = createTemplate("./static/templates/address.html", "./static/templates/txdetail.html", "./static/templates/paging.html", "./static/templates/base.html")
   552  		t[blockTpl] = createTemplate("./static/templates/block.html", "./static/templates/txdetail.html", "./static/templates/paging.html", "./static/templates/base.html")
   553  
   554  	}
   555  	t[xpubTpl] = createTemplate("./static/templates/xpub.html", "./static/templates/txdetail.html", "./static/templates/paging.html", "./static/templates/base.html")
   556  	t[mempoolTpl] = createTemplate("./static/templates/mempool.html", "./static/templates/paging.html", "./static/templates/base.html")
   557  	return t
   558  }
   559  
   560  func (s *PublicServer) postHtmlTemplateHandler(data *TemplateData, w http.ResponseWriter, r *http.Request) {
   561  	// // if SecondaryCoin is specified, set secondary_coin cookie
   562  	if data != nil && data.SecondaryCoin != "" {
   563  		http.SetCookie(w, &http.Cookie{Name: secondaryCoinCookieName, Value: data.SecondaryCoin + "=" + strconv.FormatBool(data.UseSecondaryCoin), Path: "/"})
   564  	}
   565  
   566  }
   567  
   568  func (s *PublicServer) formatAmount(a *api.Amount) string {
   569  	if a == nil {
   570  		return "0"
   571  	}
   572  	return s.chainParser.AmountToDecimalString((*big.Int)(a))
   573  }
   574  
   575  func (s *PublicServer) amountSpan(a *api.Amount, td *TemplateData, classes string) template.HTML {
   576  	primary := s.formatAmount(a)
   577  	var rv strings.Builder
   578  	appendAmountWrapperSpan(&rv, primary, td.CoinShortcut, classes)
   579  	if s.useSatsAmountFormat {
   580  		appendAmountSpanBitcoinType(&rv, "prim-amt", primary, td.CoinShortcut, "")
   581  	} else {
   582  		appendAmountSpan(&rv, "prim-amt", primary, td.CoinShortcut, "")
   583  	}
   584  	if td.SecondaryCoin != "" {
   585  		p, err := strconv.ParseFloat(primary, 64)
   586  		if err == nil {
   587  			currentSecondary := formatSecondaryAmount(p*td.CurrentSecondaryCoinRate, td)
   588  			txSecondary := ""
   589  			// if tx is specified, compute secondary amount is at the time of tx and amount with current rate is returned with class "csec-amt"
   590  			if td.Tx != nil {
   591  				if td.TxTicker == nil {
   592  					date := time.Unix(td.Tx.Blocktime, 0).UTC()
   593  					secondary := strings.ToLower(td.SecondaryCoin)
   594  					var ticker *common.CurrencyRatesTicker
   595  					tickers, err := s.fiatRates.GetTickersForTimestamps([]int64{int64(td.Tx.Blocktime)}, "", "")
   596  					if err == nil && tickers != nil && len(*tickers) > 0 {
   597  						ticker = (*tickers)[0]
   598  					}
   599  					if ticker != nil {
   600  						td.TxSecondaryCoinRate = float64(ticker.Rates[secondary])
   601  						// the ticker is from the midnight, valid for the whole day before
   602  						td.TxDate = date.Add(-1 * time.Second).Format("2006-01-02")
   603  						td.TxTicker = ticker
   604  					}
   605  				}
   606  				if td.TxSecondaryCoinRate != 0 {
   607  					txSecondary = formatSecondaryAmount(p*td.TxSecondaryCoinRate, td)
   608  				}
   609  			}
   610  			if txSecondary != "" {
   611  				appendAmountSpan(&rv, "sec-amt", txSecondary, td.SecondaryCoin, td.TxDate)
   612  				appendAmountSpan(&rv, "csec-amt", currentSecondary, td.SecondaryCoin, "")
   613  			} else {
   614  				appendAmountSpan(&rv, "sec-amt", currentSecondary, td.SecondaryCoin, "")
   615  			}
   616  		}
   617  	}
   618  	rv.WriteString("</span>")
   619  	return template.HTML(rv.String())
   620  }
   621  
   622  func (s *PublicServer) amountSatsSpan(a *api.Amount, td *TemplateData, classes string) template.HTML {
   623  	var sats string
   624  	if s.chainParser.GetChainType() == bchain.ChainEthereumType || s.chainParser.GetChainType() == bchain.ChainCoreCoinType {
   625  		sats = a.DecimalString(9) // Gwei
   626  	} else {
   627  		sats = a.String()
   628  	}
   629  	var rv strings.Builder
   630  	rv.WriteString(`<span`)
   631  	if classes != "" {
   632  		rv.WriteString(` class="`)
   633  		rv.WriteString(classes)
   634  		rv.WriteString(`"`)
   635  	}
   636  	rv.WriteString(` cc="`)
   637  	rv.WriteString(sats)
   638  	rv.WriteString(`">`)
   639  	appendAmountSpan(&rv, "", sats, "", "")
   640  	rv.WriteString("</span>")
   641  	return template.HTML(rv.String())
   642  }
   643  
   644  func (s *PublicServer) tokenAmountSpan(t *api.TokenTransfer, td *TemplateData, classes string) template.HTML {
   645  	primary := formatAmountWithDecimals(t.Value, t.Decimals)
   646  	var rv strings.Builder
   647  	appendAmountWrapperSpan(&rv, primary, t.Symbol, classes)
   648  	appendAmountSpan(&rv, "prim-amt", primary, t.Symbol, "")
   649  	if td.SecondaryCoin != "" {
   650  		var currentBase, currentSecondary, txBase, txSecondary string
   651  		p, err := strconv.ParseFloat(primary, 64)
   652  		if err == nil {
   653  			if td.CurrentTicker != nil {
   654  				// get rate from current ticker
   655  				baseRateCurrent, found := td.CurrentTicker.GetTokenRate(t.Contract)
   656  				if found {
   657  					base := p * float64(baseRateCurrent)
   658  					currentBase = strconv.FormatFloat(base, 'f', 6, 64)
   659  					currentSecondary = formatSecondaryAmount(base*td.CurrentSecondaryCoinRate, td)
   660  					// get the historical rate only if current rate exist
   661  					// it is very costly to search in DB in vain for a rate for token for which there are no exchange rates
   662  					baseRate, found := s.api.GetContractBaseRate(td.TxTicker, t.Contract, td.Tx.Blocktime)
   663  					if found {
   664  						base := p * baseRate
   665  						txBase = strconv.FormatFloat(base, 'f', 6, 64)
   666  						txSecondary = formatSecondaryAmount(base*td.TxSecondaryCoinRate, td)
   667  					}
   668  				}
   669  			}
   670  		}
   671  		if txBase != "" {
   672  			appendAmountSpan(&rv, "base-amt", txBase, td.CoinShortcut, td.TxDate)
   673  			if currentBase != "" {
   674  				appendAmountSpan(&rv, "cbase-amt", currentBase, td.CoinShortcut, "")
   675  			}
   676  		} else if currentBase != "" {
   677  			appendAmountSpan(&rv, "base-amt", currentBase, td.CoinShortcut, "")
   678  		}
   679  		if txSecondary != "" {
   680  			appendAmountSpan(&rv, "sec-amt", txSecondary, td.SecondaryCoin, td.TxDate)
   681  			if currentSecondary != "" {
   682  				appendAmountSpan(&rv, "csec-amt", currentSecondary, td.SecondaryCoin, "")
   683  			}
   684  		} else if currentSecondary != "" {
   685  			appendAmountSpan(&rv, "sec-amt", currentSecondary, td.SecondaryCoin, "")
   686  		} else {
   687  			appendAmountSpan(&rv, "sec-amt", "-", "", "")
   688  		}
   689  	}
   690  	rv.WriteString("</span>")
   691  	return template.HTML(rv.String())
   692  }
   693  
   694  func (s *PublicServer) formattedAmountSpan(a *api.Amount, d int, symbol string, td *TemplateData, classes string) template.HTML {
   695  	if symbol == td.CoinShortcut {
   696  		d = s.chainParser.AmountDecimals()
   697  	}
   698  	value := formatAmountWithDecimals(a, d)
   699  	var rv strings.Builder
   700  	appendAmountSpan(&rv, classes, value, symbol, "")
   701  	return template.HTML(rv.String())
   702  }
   703  
   704  func (s *PublicServer) summaryValuesSpan(baseValue float64, secondaryValue float64, td *TemplateData) template.HTML {
   705  	var rv strings.Builder
   706  	if secondaryValue > 0 {
   707  
   708  		appendAmountSpan(&rv, "", formatSecondaryAmount(secondaryValue, td), td.SecondaryCoin, "")
   709  		if baseValue > 0 && (s.chainParser.GetChainType() == bchain.ChainEthereumType || s.chainParser.GetChainType() == bchain.ChainCoreCoinType) {
   710  			rv.WriteString(`<span class="base-value">(`)
   711  			appendAmountSpan(&rv, "", strconv.FormatFloat(baseValue, 'f', 6, 64), td.CoinShortcut, "")
   712  			rv.WriteString(")</span>")
   713  		}
   714  	} else {
   715  		if baseValue > 0 {
   716  			appendAmountSpan(&rv, "", strconv.FormatFloat(baseValue, 'f', 6, 64), td.CoinShortcut, "")
   717  		} else {
   718  			if td.SecondaryCoin != "" {
   719  				rv.WriteString("-")
   720  			}
   721  		}
   722  	}
   723  	return template.HTML(rv.String())
   724  }
   725  
   726  func formatSecondaryAmount(a float64, td *TemplateData) string {
   727  	if td.SecondaryCoin == "BTC" || td.SecondaryCoin == "ETH" {
   728  		return strconv.FormatFloat(a, 'f', 6, 64)
   729  	}
   730  	return strconv.FormatFloat(a, 'f', 2, 64)
   731  }
   732  
   733  func getAddressAlias(a string, td *TemplateData) *api.AddressAlias {
   734  	var alias api.AddressAlias
   735  	var found bool
   736  	if td.Block != nil {
   737  		alias, found = td.Block.AddressAliases[a]
   738  	} else if td.Address != nil {
   739  		alias, found = td.Address.AddressAliases[a]
   740  	} else if td.Tx != nil {
   741  		alias, found = td.Tx.AddressAliases[a]
   742  	}
   743  	if !found {
   744  		return nil
   745  	}
   746  	return &alias
   747  }
   748  
   749  func addressAlias(a string, td *TemplateData) string {
   750  	alias := getAddressAlias(a, td)
   751  	if alias == nil {
   752  		return ""
   753  	}
   754  	return alias.Alias
   755  }
   756  
   757  func addressAliasSpan(a string, td *TemplateData) template.HTML {
   758  	var rv strings.Builder
   759  	alias := getAddressAlias(a, td)
   760  	if alias == nil {
   761  		rv.WriteString(`<span class="copyable">`)
   762  		rv.WriteString(a)
   763  	} else {
   764  		rv.WriteString(`<span class="copyable" cc="`)
   765  		rv.WriteString(a)
   766  		rv.WriteString(`" alias-type="`)
   767  		rv.WriteString(alias.Type)
   768  		rv.WriteString(`">`)
   769  		rv.WriteString(alias.Alias)
   770  	}
   771  	rv.WriteString("</span>")
   772  	return template.HTML(rv.String())
   773  }
   774  
   775  // called from template to support txdetail.html functionality
   776  func setTxToTemplateData(td *TemplateData, tx *api.Tx) *TemplateData {
   777  	td.Tx = tx
   778  	// reset the TxTicker if different Blocktime
   779  	if td.TxTicker != nil && td.TxTicker.Timestamp.Unix() != tx.Blocktime {
   780  		td.TxSecondaryCoinRate = 0
   781  		td.TxTicker = nil
   782  	}
   783  	return td
   784  }
   785  
   786  // feePerByte returns fee per vByte or Byte if vsize is unknown
   787  func feePerByte(tx *api.Tx) string {
   788  	if tx.FeesSat != nil {
   789  		if tx.VSize > 0 {
   790  			return fmt.Sprintf("%.2f sat/vByte", float64(tx.FeesSat.AsInt64())/float64(tx.VSize))
   791  		}
   792  		if tx.Size > 0 {
   793  			return fmt.Sprintf("%.2f sat/Byte", float64(tx.FeesSat.AsInt64())/float64(tx.Size))
   794  		}
   795  	}
   796  	return ""
   797  }
   798  
   799  // isOwnAddress returns true if the address is the one that is being shown in the explorer
   800  func isOwnAddress(td *TemplateData, a string) bool {
   801  	return a == td.AddrStr
   802  }
   803  
   804  // called from template, returns count of token transfers of given type in a tx
   805  func tokenTransfersCount(tx *api.Tx, t bchain.TokenTypeName) int {
   806  	count := 0
   807  	for i := range tx.TokenTransfers {
   808  		if tx.TokenTransfers[i].Type == t {
   809  			count++
   810  		}
   811  	}
   812  	return count
   813  }
   814  
   815  // called from template, returns count of tokens in array of given type
   816  func tokenCount(tokens []api.Token, t bchain.TokenTypeName) int {
   817  	count := 0
   818  	for i := range tokens {
   819  		if tokens[i].Type == t {
   820  			count++
   821  		}
   822  	}
   823  	return count
   824  }
   825  
   826  func jsStr(s string) template.JSStr {
   827  	return template.JSStr(s)
   828  }
   829  
   830  func (s *PublicServer) explorerTx(w http.ResponseWriter, r *http.Request) (tpl, *TemplateData, error) {
   831  	var tx *api.Tx
   832  	var err error
   833  	s.metrics.ExplorerViews.With(common.Labels{"action": "tx"}).Inc()
   834  	if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 {
   835  		txid := r.URL.Path[i+1:]
   836  		tx, err = s.api.GetTransaction(txid, false, true)
   837  		if err != nil {
   838  			return errorTpl, nil, err
   839  		}
   840  	}
   841  	data := s.newTemplateData(r)
   842  	data.Tx = tx
   843  	return txTpl, data, nil
   844  }
   845  
   846  func (s *PublicServer) explorerSpendingTx(w http.ResponseWriter, r *http.Request) (tpl, *TemplateData, error) {
   847  	s.metrics.ExplorerViews.With(common.Labels{"action": "spendingtx"}).Inc()
   848  	var err error
   849  	parts := strings.Split(r.URL.Path, "/")
   850  	if len(parts) > 2 {
   851  		tx := parts[len(parts)-2]
   852  		n, ec := strconv.Atoi(parts[len(parts)-1])
   853  		if ec == nil {
   854  			spendingTx, err := s.api.GetSpendingTxid(tx, n)
   855  			if err == nil && spendingTx != "" {
   856  				http.Redirect(w, r, joinURL("/tx/", spendingTx), http.StatusFound)
   857  				return noTpl, nil, nil
   858  			}
   859  		}
   860  	}
   861  	if err == nil {
   862  		err = api.NewAPIError("Transaction not found", true)
   863  	}
   864  	return errorTpl, nil, err
   865  }
   866  
   867  func (s *PublicServer) getAddressQueryParams(r *http.Request, accountDetails api.AccountDetails, maxPageSize int) (int, int, api.AccountDetails, *api.AddressFilter, string, int) {
   868  	var voutFilter = api.AddressFilterVoutOff
   869  	page, ec := strconv.Atoi(r.URL.Query().Get("page"))
   870  	if ec != nil {
   871  		page = 0
   872  	}
   873  	pageSize, ec := strconv.Atoi(r.URL.Query().Get("pageSize"))
   874  	if ec != nil || pageSize > maxPageSize {
   875  		pageSize = maxPageSize
   876  	}
   877  	from, ec := strconv.Atoi(r.URL.Query().Get("from"))
   878  	if ec != nil {
   879  		from = 0
   880  	}
   881  	to, ec := strconv.Atoi(r.URL.Query().Get("to"))
   882  	if ec != nil {
   883  		to = 0
   884  	}
   885  	filterParam := r.URL.Query().Get("filter")
   886  	if len(filterParam) > 0 {
   887  		if filterParam == "inputs" {
   888  			voutFilter = api.AddressFilterVoutInputs
   889  		} else if filterParam == "outputs" {
   890  			voutFilter = api.AddressFilterVoutOutputs
   891  		} else {
   892  			voutFilter, ec = strconv.Atoi(filterParam)
   893  			if ec != nil || voutFilter < 0 {
   894  				voutFilter = api.AddressFilterVoutOff
   895  			}
   896  		}
   897  	}
   898  	switch r.URL.Query().Get("details") {
   899  	case "basic":
   900  		accountDetails = api.AccountDetailsBasic
   901  	case "tokens":
   902  		accountDetails = api.AccountDetailsTokens
   903  	case "tokenBalances":
   904  		accountDetails = api.AccountDetailsTokenBalances
   905  	case "txids":
   906  		accountDetails = api.AccountDetailsTxidHistory
   907  	case "txslight":
   908  		accountDetails = api.AccountDetailsTxHistoryLight
   909  	case "txs":
   910  		accountDetails = api.AccountDetailsTxHistory
   911  	}
   912  	tokensToReturn := api.TokensToReturnNonzeroBalance
   913  	switch r.URL.Query().Get("tokens") {
   914  	case "derived":
   915  		tokensToReturn = api.TokensToReturnDerived
   916  	case "used":
   917  		tokensToReturn = api.TokensToReturnUsed
   918  	case "nonzero":
   919  		tokensToReturn = api.TokensToReturnNonzeroBalance
   920  	}
   921  	gap, ec := strconv.Atoi(r.URL.Query().Get("gap"))
   922  	if ec != nil {
   923  		gap = 0
   924  	}
   925  	contract := r.URL.Query().Get("contract")
   926  	return page, pageSize, accountDetails, &api.AddressFilter{
   927  		Vout:           voutFilter,
   928  		TokensToReturn: tokensToReturn,
   929  		FromHeight:     uint32(from),
   930  		ToHeight:       uint32(to),
   931  		Contract:       contract,
   932  	}, filterParam, gap
   933  }
   934  
   935  func (s *PublicServer) explorerAddress(w http.ResponseWriter, r *http.Request) (tpl, *TemplateData, error) {
   936  	var addressParam string
   937  	i := strings.LastIndexByte(r.URL.Path, '/')
   938  	if i > 0 {
   939  		addressParam = r.URL.Path[i+1:]
   940  	}
   941  	if len(addressParam) == 0 {
   942  		return errorTpl, nil, api.NewAPIError("Missing address", true)
   943  	}
   944  	s.metrics.ExplorerViews.With(common.Labels{"action": "address"}).Inc()
   945  	page, _, _, filter, filterParam, _ := s.getAddressQueryParams(r, api.AccountDetailsTxHistoryLight, txsOnPage)
   946  	// do not allow details to be changed by query params
   947  	data := s.newTemplateData(r)
   948  	address, err := s.api.GetAddress(addressParam, page, txsOnPage, api.AccountDetailsTxHistoryLight, filter, strings.ToLower(data.SecondaryCoin))
   949  	if err != nil {
   950  		return errorTpl, nil, err
   951  	}
   952  	data.AddrStr = address.AddrStr
   953  	data.Address = address
   954  	data.Page = address.Page
   955  	data.PagingRange, data.PrevPage, data.NextPage = getPagingRange(address.Page, address.TotalPages)
   956  	if filterParam == "" && filter.Vout > -1 {
   957  		filterParam = strconv.Itoa(filter.Vout)
   958  	}
   959  	if filterParam != "" {
   960  		data.PageParams = template.URL("&filter=" + filterParam)
   961  		data.Address.Filter = filterParam
   962  	}
   963  	return addressTpl, data, nil
   964  }
   965  
   966  func (s *PublicServer) explorerNftDetail(w http.ResponseWriter, r *http.Request) (tpl, *TemplateData, error) {
   967  	parts := strings.Split(r.URL.Path, "/")
   968  	if len(parts) < 3 {
   969  		return errorTpl, nil, api.NewAPIError("Missing parameters", true)
   970  	}
   971  	tokenId := parts[len(parts)-1]
   972  	contract := parts[len(parts)-2]
   973  	uri, ci, err := s.api.GetEthereumTokenURI(contract, tokenId)
   974  	s.metrics.ExplorerViews.With(common.Labels{"action": "nftDetail"}).Inc()
   975  	if err != nil {
   976  		return errorTpl, nil, api.NewAPIError(err.Error(), true)
   977  	}
   978  	if ci == nil {
   979  		return errorTpl, nil, api.NewAPIError(fmt.Sprintf("Unknown contract %s", contract), true)
   980  	}
   981  	data := s.newTemplateData(r)
   982  	data.TokenId = tokenId
   983  	data.ContractInfo = ci
   984  	data.URI = uri
   985  	return nftDetailTpl, data, nil
   986  }
   987  
   988  func (s *PublicServer) explorerXpub(w http.ResponseWriter, r *http.Request) (tpl, *TemplateData, error) {
   989  	var xpub string
   990  	i := strings.LastIndex(r.URL.Path, "xpub/")
   991  	if i > 0 {
   992  		xpub = r.URL.Path[i+5:]
   993  	}
   994  	if len(xpub) == 0 {
   995  		return errorTpl, nil, api.NewAPIError("Missing xpub", true)
   996  	}
   997  	s.metrics.ExplorerViews.With(common.Labels{"action": "xpub"}).Inc()
   998  	// do not allow txsOnPage and details to be changed by query params
   999  	page, _, _, filter, filterParam, gap := s.getAddressQueryParams(r, api.AccountDetailsTxHistoryLight, txsOnPage)
  1000  	data := s.newTemplateData(r)
  1001  	address, err := s.api.GetXpubAddress(xpub, page, txsOnPage, api.AccountDetailsTxHistoryLight, filter, gap, strings.ToLower(data.SecondaryCoin))
  1002  	if err != nil {
  1003  		if err == api.ErrUnsupportedXpub {
  1004  			err = api.NewAPIError("XPUB functionality is not supported", true)
  1005  		}
  1006  		return errorTpl, nil, err
  1007  	}
  1008  	data.AddrStr = address.AddrStr
  1009  	data.Address = address
  1010  	data.Page = address.Page
  1011  	data.PagingRange, data.PrevPage, data.NextPage = getPagingRange(address.Page, address.TotalPages)
  1012  	if filterParam != "" {
  1013  		data.PageParams = template.URL("&filter=" + filterParam)
  1014  		data.Address.Filter = filterParam
  1015  	}
  1016  	data.NonZeroBalanceTokens = filter.TokensToReturn == api.TokensToReturnNonzeroBalance
  1017  	return xpubTpl, data, nil
  1018  }
  1019  
  1020  func (s *PublicServer) explorerBlocks(w http.ResponseWriter, r *http.Request) (tpl, *TemplateData, error) {
  1021  	var blocks *api.Blocks
  1022  	var err error
  1023  	s.metrics.ExplorerViews.With(common.Labels{"action": "blocks"}).Inc()
  1024  	page, ec := strconv.Atoi(r.URL.Query().Get("page"))
  1025  	if ec != nil {
  1026  		page = 0
  1027  	}
  1028  	blocks, err = s.api.GetBlocks(page, blocksOnPage)
  1029  	if err != nil {
  1030  		return errorTpl, nil, err
  1031  	}
  1032  	data := s.newTemplateData(r)
  1033  	data.Blocks = blocks
  1034  	data.Page = blocks.Page
  1035  	data.PagingRange, data.PrevPage, data.NextPage = getPagingRange(blocks.Page, blocks.TotalPages)
  1036  	return blocksTpl, data, nil
  1037  }
  1038  
  1039  func (s *PublicServer) explorerBlock(w http.ResponseWriter, r *http.Request) (tpl, *TemplateData, error) {
  1040  	var block *api.Block
  1041  	var err error
  1042  	s.metrics.ExplorerViews.With(common.Labels{"action": "block"}).Inc()
  1043  	if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 {
  1044  		page, ec := strconv.Atoi(r.URL.Query().Get("page"))
  1045  		if ec != nil {
  1046  			page = 0
  1047  		}
  1048  		block, err = s.api.GetBlock(r.URL.Path[i+1:], page, txsOnPage)
  1049  		if err != nil {
  1050  			return errorTpl, nil, err
  1051  		}
  1052  	}
  1053  	data := s.newTemplateData(r)
  1054  	data.Block = block
  1055  	data.Page = block.Page
  1056  	data.PagingRange, data.PrevPage, data.NextPage = getPagingRange(block.Page, block.TotalPages)
  1057  	return blockTpl, data, nil
  1058  }
  1059  
  1060  func (s *PublicServer) explorerIndex(w http.ResponseWriter, r *http.Request) (tpl, *TemplateData, error) {
  1061  	var si *api.SystemInfo
  1062  	var err error
  1063  	s.metrics.ExplorerViews.With(common.Labels{"action": "index"}).Inc()
  1064  	si, err = s.api.GetSystemInfo(false)
  1065  	if err != nil {
  1066  		return errorTpl, nil, err
  1067  	}
  1068  	data := s.newTemplateData(r)
  1069  	data.Info = si
  1070  	return indexTpl, data, nil
  1071  }
  1072  
  1073  func (s *PublicServer) explorerSearch(w http.ResponseWriter, r *http.Request) (tpl, *TemplateData, error) {
  1074  	q := strings.TrimSpace(r.URL.Query().Get("q"))
  1075  	var tx *api.Tx
  1076  	var address *api.Address
  1077  	var block *api.Block
  1078  	var err error
  1079  	s.metrics.ExplorerViews.With(common.Labels{"action": "search"}).Inc()
  1080  	if len(q) > 0 {
  1081  		address, err = s.api.GetXpubAddress(q, 0, 1, api.AccountDetailsBasic, &api.AddressFilter{Vout: api.AddressFilterVoutOff}, 0, "")
  1082  		if err == nil {
  1083  			http.Redirect(w, r, joinURL("/xpub/", url.QueryEscape(address.AddrStr)), http.StatusFound)
  1084  			return noTpl, nil, nil
  1085  		}
  1086  		block, err = s.api.GetBlock(q, 0, 1)
  1087  		if err == nil {
  1088  			http.Redirect(w, r, joinURL("/block/", block.Hash), http.StatusFound)
  1089  			return noTpl, nil, nil
  1090  		}
  1091  		tx, err = s.api.GetTransaction(q, false, false)
  1092  		if err == nil {
  1093  			http.Redirect(w, r, joinURL("/tx/", tx.Txid), http.StatusFound)
  1094  			return noTpl, nil, nil
  1095  		}
  1096  		address, err = s.api.GetAddress(q, 0, 1, api.AccountDetailsBasic, &api.AddressFilter{Vout: api.AddressFilterVoutOff}, "")
  1097  		if err == nil {
  1098  			http.Redirect(w, r, joinURL("/address/", address.AddrStr), http.StatusFound)
  1099  			return noTpl, nil, nil
  1100  		}
  1101  	}
  1102  	return errorTpl, nil, api.NewAPIError(fmt.Sprintf("No matching records found for '%v'", q), true)
  1103  }
  1104  
  1105  func (s *PublicServer) explorerSendTx(w http.ResponseWriter, r *http.Request) (tpl, *TemplateData, error) {
  1106  	s.metrics.ExplorerViews.With(common.Labels{"action": "sendtx"}).Inc()
  1107  	data := s.newTemplateData(r)
  1108  	if r.Method == http.MethodPost {
  1109  		err := r.ParseForm()
  1110  		if err != nil {
  1111  			return sendTransactionTpl, data, err
  1112  		}
  1113  		hex := r.FormValue("hex")
  1114  		if len(hex) > 0 {
  1115  			res, err := s.chain.SendRawTransaction(hex)
  1116  			if err != nil {
  1117  				data.SendTxHex = hex
  1118  				data.Error = &api.APIError{Text: err.Error(), Public: true}
  1119  				return sendTransactionTpl, data, nil
  1120  			}
  1121  			data.Status = "Transaction sent, result " + res
  1122  		}
  1123  	}
  1124  	return sendTransactionTpl, data, nil
  1125  }
  1126  
  1127  func (s *PublicServer) explorerMempool(w http.ResponseWriter, r *http.Request) (tpl, *TemplateData, error) {
  1128  	var mempoolTxids *api.MempoolTxids
  1129  	var err error
  1130  	s.metrics.ExplorerViews.With(common.Labels{"action": "mempool"}).Inc()
  1131  	page, ec := strconv.Atoi(r.URL.Query().Get("page"))
  1132  	if ec != nil {
  1133  		page = 0
  1134  	}
  1135  	mempoolTxids, err = s.api.GetMempool(page, mempoolTxsOnPage)
  1136  	if err != nil {
  1137  		return errorTpl, nil, err
  1138  	}
  1139  	data := s.newTemplateData(r)
  1140  	data.MempoolTxids = mempoolTxids
  1141  	data.Page = mempoolTxids.Page
  1142  	data.PagingRange, data.PrevPage, data.NextPage = getPagingRange(mempoolTxids.Page, mempoolTxids.TotalPages)
  1143  	return mempoolTpl, data, nil
  1144  }
  1145  
  1146  func getPagingRange(page int, total int) ([]int, int, int) {
  1147  	// total==-1 means total is unknown, show only prev/next buttons
  1148  	if total >= 0 && total < 2 {
  1149  		return nil, 0, 0
  1150  	}
  1151  	var r []int
  1152  	pp, np := page-1, page+1
  1153  	if pp < 1 {
  1154  		pp = 1
  1155  	}
  1156  	if total > 0 {
  1157  		if np > total {
  1158  			np = total
  1159  		}
  1160  		r = make([]int, 0, 8)
  1161  		if total < 6 {
  1162  			for i := 1; i <= total; i++ {
  1163  				r = append(r, i)
  1164  			}
  1165  		} else {
  1166  			r = append(r, 1)
  1167  			if page > 3 {
  1168  				r = append(r, 0)
  1169  			}
  1170  			if pp == 1 {
  1171  				if page == 1 {
  1172  					r = append(r, np)
  1173  					r = append(r, np+1)
  1174  					r = append(r, np+2)
  1175  				} else {
  1176  					r = append(r, page)
  1177  					r = append(r, np)
  1178  					r = append(r, np+1)
  1179  				}
  1180  			} else if np == total {
  1181  				if page == total {
  1182  					r = append(r, pp-2)
  1183  					r = append(r, pp-1)
  1184  					r = append(r, pp)
  1185  				} else {
  1186  					r = append(r, pp-1)
  1187  					r = append(r, pp)
  1188  					r = append(r, page)
  1189  				}
  1190  			} else {
  1191  				r = append(r, pp)
  1192  				r = append(r, page)
  1193  				r = append(r, np)
  1194  			}
  1195  			if page <= total-3 {
  1196  				r = append(r, 0)
  1197  			}
  1198  			r = append(r, total)
  1199  		}
  1200  	}
  1201  	return r, pp, np
  1202  }
  1203  
  1204  func (s *PublicServer) apiIndex(r *http.Request, apiVersion int) (interface{}, error) {
  1205  	s.metrics.ExplorerViews.With(common.Labels{"action": "api-index"}).Inc()
  1206  	return s.api.GetSystemInfo(false)
  1207  }
  1208  
  1209  func (s *PublicServer) apiBlockIndex(r *http.Request, apiVersion int) (interface{}, error) {
  1210  	type resBlockIndex struct {
  1211  		BlockHash string `json:"blockHash"`
  1212  	}
  1213  	var err error
  1214  	var hash string
  1215  	height := -1
  1216  	if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 {
  1217  		if h, err := strconv.Atoi(r.URL.Path[i+1:]); err == nil {
  1218  			height = h
  1219  		}
  1220  	}
  1221  	if height >= 0 {
  1222  		hash, err = s.db.GetBlockHash(uint32(height))
  1223  	} else {
  1224  		_, hash, err = s.db.GetBestBlock()
  1225  	}
  1226  	if err != nil {
  1227  		glog.Error(err)
  1228  		return nil, err
  1229  	}
  1230  	return resBlockIndex{
  1231  		BlockHash: hash,
  1232  	}, nil
  1233  }
  1234  
  1235  func (s *PublicServer) apiTx(r *http.Request, apiVersion int) (interface{}, error) {
  1236  	var txid string
  1237  	i := strings.LastIndexByte(r.URL.Path, '/')
  1238  	if i > 0 {
  1239  		txid = r.URL.Path[i+1:]
  1240  	}
  1241  	if len(txid) == 0 {
  1242  		return nil, api.NewAPIError("Missing txid", true)
  1243  	}
  1244  	var tx *api.Tx
  1245  	var err error
  1246  	s.metrics.ExplorerViews.With(common.Labels{"action": "api-tx"}).Inc()
  1247  	spendingTxs := false
  1248  	p := r.URL.Query().Get("spending")
  1249  	if len(p) > 0 {
  1250  		spendingTxs, err = strconv.ParseBool(p)
  1251  		if err != nil {
  1252  			return nil, api.NewAPIError("Parameter 'spending' cannot be converted to boolean", true)
  1253  		}
  1254  	}
  1255  	tx, err = s.api.GetTransaction(txid, spendingTxs, false)
  1256  	if err == nil && apiVersion == apiV1 {
  1257  		return s.api.TxToV1(tx), nil
  1258  	}
  1259  	return tx, err
  1260  }
  1261  
  1262  func (s *PublicServer) apiTxSpecific(r *http.Request, apiVersion int) (interface{}, error) {
  1263  	var txid string
  1264  	i := strings.LastIndexByte(r.URL.Path, '/')
  1265  	if i > 0 {
  1266  		txid = r.URL.Path[i+1:]
  1267  	}
  1268  	if len(txid) == 0 {
  1269  		return nil, api.NewAPIError("Missing txid", true)
  1270  	}
  1271  	var tx json.RawMessage
  1272  	var err error
  1273  	s.metrics.ExplorerViews.With(common.Labels{"action": "api-tx-specific"}).Inc()
  1274  	tx, err = s.chain.GetTransactionSpecific(&bchain.Tx{Txid: txid})
  1275  	if err == bchain.ErrTxNotFound {
  1276  		return nil, api.NewAPIError(fmt.Sprintf("Transaction '%v' not found", txid), true)
  1277  	}
  1278  	return tx, err
  1279  }
  1280  
  1281  func (s *PublicServer) apiAddress(r *http.Request, apiVersion int) (interface{}, error) {
  1282  	var addressParam string
  1283  	i := strings.LastIndexByte(r.URL.Path, '/')
  1284  	if i > 0 {
  1285  		addressParam = r.URL.Path[i+1:]
  1286  	}
  1287  	if len(addressParam) == 0 {
  1288  		return nil, api.NewAPIError("Missing address", true)
  1289  	}
  1290  	var address *api.Address
  1291  	var err error
  1292  	s.metrics.ExplorerViews.With(common.Labels{"action": "api-address"}).Inc()
  1293  	page, pageSize, details, filter, _, _ := s.getAddressQueryParams(r, api.AccountDetailsTxidHistory, txsInAPI)
  1294  	secondaryCoin := strings.ToLower(r.URL.Query().Get("secondary"))
  1295  	address, err = s.api.GetAddress(addressParam, page, pageSize, details, filter, secondaryCoin)
  1296  	if err == nil && apiVersion == apiV1 {
  1297  		return s.api.AddressToV1(address), nil
  1298  	}
  1299  	return address, err
  1300  }
  1301  
  1302  func (s *PublicServer) apiXpub(r *http.Request, apiVersion int) (interface{}, error) {
  1303  	var xpub string
  1304  	i := strings.LastIndex(r.URL.Path, "xpub/")
  1305  	if i > 0 {
  1306  		xpub = r.URL.Path[i+5:]
  1307  	}
  1308  	if len(xpub) == 0 {
  1309  		return nil, api.NewAPIError("Missing xpub", true)
  1310  	}
  1311  	var address *api.Address
  1312  	var err error
  1313  	s.metrics.ExplorerViews.With(common.Labels{"action": "api-xpub"}).Inc()
  1314  	page, pageSize, details, filter, _, gap := s.getAddressQueryParams(r, api.AccountDetailsTxidHistory, txsInAPI)
  1315  	secondaryCoin := strings.ToLower(r.URL.Query().Get("secondary"))
  1316  	address, err = s.api.GetXpubAddress(xpub, page, pageSize, details, filter, gap, secondaryCoin)
  1317  	if err == nil && apiVersion == apiV1 {
  1318  		return s.api.AddressToV1(address), nil
  1319  	}
  1320  	if err == api.ErrUnsupportedXpub {
  1321  		err = api.NewAPIError("XPUB functionality is not supported", true)
  1322  	}
  1323  	return address, err
  1324  }
  1325  
  1326  func (s *PublicServer) apiUtxo(r *http.Request, apiVersion int) (interface{}, error) {
  1327  	var utxo []api.Utxo
  1328  	var err error
  1329  	if i := strings.LastIndex(r.URL.Path, "utxo/"); i > 0 {
  1330  		desc := r.URL.Path[i+5:]
  1331  		onlyConfirmed := false
  1332  		c := r.URL.Query().Get("confirmed")
  1333  		if len(c) > 0 {
  1334  			onlyConfirmed, err = strconv.ParseBool(c)
  1335  			if err != nil {
  1336  				return nil, api.NewAPIError("Parameter 'confirmed' cannot be converted to boolean", true)
  1337  			}
  1338  		}
  1339  		gap, ec := strconv.Atoi(r.URL.Query().Get("gap"))
  1340  		if ec != nil {
  1341  			gap = 0
  1342  		}
  1343  		utxo, err = s.api.GetXpubUtxo(desc, onlyConfirmed, gap)
  1344  		if err == nil {
  1345  			s.metrics.ExplorerViews.With(common.Labels{"action": "api-xpub-utxo"}).Inc()
  1346  		} else {
  1347  			utxo, err = s.api.GetAddressUtxo(desc, onlyConfirmed)
  1348  			s.metrics.ExplorerViews.With(common.Labels{"action": "api-address-utxo"}).Inc()
  1349  		}
  1350  		if err == nil && apiVersion == apiV1 {
  1351  			return s.api.AddressUtxoToV1(utxo), nil
  1352  		}
  1353  	}
  1354  	return utxo, err
  1355  }
  1356  
  1357  func (s *PublicServer) apiBalanceHistory(r *http.Request, apiVersion int) (interface{}, error) {
  1358  	var history []api.BalanceHistory
  1359  	var fromTimestamp, toTimestamp int64
  1360  	var err error
  1361  	if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 {
  1362  		gap, ec := strconv.Atoi(r.URL.Query().Get("gap"))
  1363  		if ec != nil {
  1364  			gap = 0
  1365  		}
  1366  		from := r.URL.Query().Get("from")
  1367  		if from != "" {
  1368  			fromTimestamp, err = strconv.ParseInt(from, 10, 64)
  1369  			if err != nil {
  1370  				return history, err
  1371  			}
  1372  		}
  1373  		to := r.URL.Query().Get("to")
  1374  		if to != "" {
  1375  			toTimestamp, err = strconv.ParseInt(to, 10, 64)
  1376  			if err != nil {
  1377  				return history, err
  1378  			}
  1379  		}
  1380  		var groupBy uint64
  1381  		groupBy, err = strconv.ParseUint(r.URL.Query().Get("groupBy"), 10, 32)
  1382  		if err != nil || groupBy == 0 {
  1383  			groupBy = 3600
  1384  		}
  1385  		fiat := r.URL.Query().Get("fiatcurrency")
  1386  		var fiatArray []string
  1387  		if fiat != "" {
  1388  			fiatArray = []string{fiat}
  1389  		}
  1390  		history, err = s.api.GetXpubBalanceHistory(r.URL.Path[i+1:], fromTimestamp, toTimestamp, fiatArray, gap, uint32(groupBy))
  1391  		if err == nil {
  1392  			s.metrics.ExplorerViews.With(common.Labels{"action": "api-xpub-balancehistory"}).Inc()
  1393  		} else {
  1394  			history, err = s.api.GetBalanceHistory(r.URL.Path[i+1:], fromTimestamp, toTimestamp, fiatArray, uint32(groupBy))
  1395  			s.metrics.ExplorerViews.With(common.Labels{"action": "api-address-balancehistory"}).Inc()
  1396  		}
  1397  	}
  1398  	return history, err
  1399  }
  1400  
  1401  func (s *PublicServer) apiBlock(r *http.Request, apiVersion int) (interface{}, error) {
  1402  	var block *api.Block
  1403  	var err error
  1404  	s.metrics.ExplorerViews.With(common.Labels{"action": "api-block"}).Inc()
  1405  	if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 {
  1406  		page, ec := strconv.Atoi(r.URL.Query().Get("page"))
  1407  		if ec != nil {
  1408  			page = 0
  1409  		}
  1410  		block, err = s.api.GetBlock(r.URL.Path[i+1:], page, txsInAPI)
  1411  		if err == nil && apiVersion == apiV1 {
  1412  			return s.api.BlockToV1(block), nil
  1413  		}
  1414  	}
  1415  	return block, err
  1416  }
  1417  
  1418  func (s *PublicServer) apiBlockRaw(r *http.Request, apiVersion int) (interface{}, error) {
  1419  	var block *api.BlockRaw
  1420  	var err error
  1421  	s.metrics.ExplorerViews.With(common.Labels{"action": "api-block-raw"}).Inc()
  1422  	if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 {
  1423  		block, err = s.api.GetBlockRaw(r.URL.Path[i+1:])
  1424  	}
  1425  	return block, err
  1426  }
  1427  
  1428  func (s *PublicServer) apiFeeStats(r *http.Request, apiVersion int) (interface{}, error) {
  1429  	var feeStats *api.FeeStats
  1430  	var err error
  1431  	s.metrics.ExplorerViews.With(common.Labels{"action": "api-feestats"}).Inc()
  1432  	if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 {
  1433  		feeStats, err = s.api.GetFeeStats(r.URL.Path[i+1:])
  1434  	}
  1435  	return feeStats, err
  1436  }
  1437  
  1438  type resultSendTransaction struct {
  1439  	Result string `json:"result"`
  1440  }
  1441  
  1442  func (s *PublicServer) apiSendTx(r *http.Request, apiVersion int) (interface{}, error) {
  1443  	var err error
  1444  	var res resultSendTransaction
  1445  	var hex string
  1446  	s.metrics.ExplorerViews.With(common.Labels{"action": "api-sendtx"}).Inc()
  1447  	if r.Method == http.MethodPost {
  1448  		data, err := io.ReadAll(r.Body)
  1449  		if err != nil {
  1450  			return nil, api.NewAPIError("Missing tx blob", true)
  1451  		}
  1452  		hex = string(data)
  1453  	} else {
  1454  		if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 {
  1455  			hex = r.URL.Path[i+1:]
  1456  		}
  1457  	}
  1458  	if len(hex) > 0 {
  1459  		res.Result, err = s.chain.SendRawTransaction(hex)
  1460  		if err != nil {
  1461  			return nil, api.NewAPIError(err.Error(), true)
  1462  		}
  1463  		return res, nil
  1464  	}
  1465  	return nil, api.NewAPIError("Missing tx blob", true)
  1466  }
  1467  
  1468  // apiAvailableVsCurrencies returns a list of available versus currencies
  1469  func (s *PublicServer) apiAvailableVsCurrencies(r *http.Request, apiVersion int) (interface{}, error) {
  1470  	s.metrics.ExplorerViews.With(common.Labels{"action": "api-tickers-list"}).Inc()
  1471  	timestampString := strings.ToLower(r.URL.Query().Get("timestamp"))
  1472  	timestamp, err := strconv.ParseInt(timestampString, 10, 64)
  1473  	if err != nil {
  1474  		return nil, api.NewAPIError("Parameter \"timestamp\" is not a valid Unix timestamp.", true)
  1475  	}
  1476  	token := strings.ToLower(r.URL.Query().Get("token"))
  1477  	result, err := s.api.GetAvailableVsCurrencies(timestamp, token)
  1478  	return result, err
  1479  }
  1480  
  1481  // apiTickers returns FiatRates ticker prices for the specified block or timestamp.
  1482  func (s *PublicServer) apiTickers(r *http.Request, apiVersion int) (interface{}, error) {
  1483  	var result *api.FiatTicker
  1484  	var err error
  1485  
  1486  	currency := strings.ToLower(r.URL.Query().Get("currency"))
  1487  	var currencies []string
  1488  	if currency != "" {
  1489  		currencies = []string{currency}
  1490  	}
  1491  	token := strings.ToLower(r.URL.Query().Get("token"))
  1492  
  1493  	if block := r.URL.Query().Get("block"); block != "" {
  1494  		// Get tickers for specified block height or block hash
  1495  		s.metrics.ExplorerViews.With(common.Labels{"action": "api-tickers-block"}).Inc()
  1496  		result, err = s.api.GetFiatRatesForBlockID(block, currencies, token)
  1497  	} else if timestampString := r.URL.Query().Get("timestamp"); timestampString != "" {
  1498  		// Get tickers for specified timestamp
  1499  		s.metrics.ExplorerViews.With(common.Labels{"action": "api-tickers-date"}).Inc()
  1500  
  1501  		timestamp, err := strconv.ParseInt(timestampString, 10, 64)
  1502  		if err != nil {
  1503  			return nil, api.NewAPIError("Parameter 'timestamp' is not a valid Unix timestamp.", true)
  1504  		}
  1505  
  1506  		resultTickers, err := s.api.GetFiatRatesForTimestamps([]int64{timestamp}, currencies, token)
  1507  		if err != nil {
  1508  			return nil, err
  1509  		}
  1510  		result = &resultTickers.Tickers[0]
  1511  	} else {
  1512  		// No parameters - get the latest available ticker
  1513  		s.metrics.ExplorerViews.With(common.Labels{"action": "api-tickers-last"}).Inc()
  1514  		result, err = s.api.GetCurrentFiatRates(currencies, token)
  1515  	}
  1516  	if err != nil {
  1517  		return nil, err
  1518  	}
  1519  	return result, nil
  1520  }
  1521  
  1522  // apiMultiTickers returns FiatRates ticker prices for the specified comma separated list of timestamps.
  1523  func (s *PublicServer) apiMultiTickers(r *http.Request, apiVersion int) (interface{}, error) {
  1524  	var result []api.FiatTicker
  1525  	var err error
  1526  
  1527  	currency := strings.ToLower(r.URL.Query().Get("currency"))
  1528  	var currencies []string
  1529  	if currency != "" {
  1530  		currencies = []string{currency}
  1531  	}
  1532  	token := strings.ToLower(r.URL.Query().Get("token"))
  1533  	if timestampString := r.URL.Query().Get("timestamp"); timestampString != "" {
  1534  		// Get tickers for specified timestamp
  1535  		s.metrics.ExplorerViews.With(common.Labels{"action": "api-multi-tickers-date"}).Inc()
  1536  		timestamps := strings.Split(timestampString, ",")
  1537  		t := make([]int64, len(timestamps))
  1538  		for i := range timestamps {
  1539  			t[i], err = strconv.ParseInt(timestamps[i], 10, 64)
  1540  			if err != nil {
  1541  				return nil, api.NewAPIError("Parameter 'timestamp' does not contain a valid Unix timestamp.", true)
  1542  			}
  1543  		}
  1544  		resultTickers, err := s.api.GetFiatRatesForTimestamps(t, currencies, token)
  1545  		if err != nil {
  1546  			return nil, err
  1547  		}
  1548  		result = resultTickers.Tickers
  1549  	} else {
  1550  		return nil, api.NewAPIError("Parameter 'timestamp' is missing.", true)
  1551  	}
  1552  	return result, nil
  1553  }
  1554  
  1555  type resultEstimateFeeAsString struct {
  1556  	Result string `json:"result"`
  1557  }
  1558  
  1559  func (s *PublicServer) apiEstimateFee(r *http.Request, apiVersion int) (interface{}, error) {
  1560  	var res resultEstimateFeeAsString
  1561  	s.metrics.ExplorerViews.With(common.Labels{"action": "api-estimatefee"}).Inc()
  1562  	if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 {
  1563  		b := r.URL.Path[i+1:]
  1564  		if len(b) > 0 {
  1565  			blocks, err := strconv.Atoi(b)
  1566  			if err != nil {
  1567  				return nil, api.NewAPIError("Parameter 'number of blocks' is not a number", true)
  1568  			}
  1569  			conservative := true
  1570  			c := r.URL.Query().Get("conservative")
  1571  			if len(c) > 0 {
  1572  				conservative, err = strconv.ParseBool(c)
  1573  				if err != nil {
  1574  					return nil, api.NewAPIError("Parameter 'conservative' cannot be converted to boolean", true)
  1575  				}
  1576  			}
  1577  			var fee big.Int
  1578  			fee, err = s.chain.EstimateSmartFee(blocks, conservative)
  1579  			if err != nil {
  1580  				fee, err = s.chain.EstimateFee(blocks)
  1581  				if err != nil {
  1582  					return nil, err
  1583  				}
  1584  			}
  1585  			res.Result = s.chainParser.AmountToDecimalString(&fee)
  1586  			return res, nil
  1587  		}
  1588  	}
  1589  	return nil, api.NewAPIError("Missing parameter 'number of blocks'", true)
  1590  }