github.com/dawnbass68/maddcash@v0.0.0-20201001105353-c91c12cb36e5/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 "txs":
   629  		accountDetails = api.AccountDetailsTxHistory
   630  	}
   631  	tokensToReturn := api.TokensToReturnNonzeroBalance
   632  	switch r.URL.Query().Get("tokens") {
   633  	case "derived":
   634  		tokensToReturn = api.TokensToReturnDerived
   635  	case "used":
   636  		tokensToReturn = api.TokensToReturnUsed
   637  	case "nonzero":
   638  		tokensToReturn = api.TokensToReturnNonzeroBalance
   639  	}
   640  	gap, ec := strconv.Atoi(r.URL.Query().Get("gap"))
   641  	if ec != nil {
   642  		gap = 0
   643  	}
   644  	return page, pageSize, accountDetails, &api.AddressFilter{
   645  		Vout:           voutFilter,
   646  		TokensToReturn: tokensToReturn,
   647  		FromHeight:     uint32(from),
   648  		ToHeight:       uint32(to),
   649  	}, filterParam, gap
   650  }
   651  
   652  func (s *PublicServer) explorerAddress(w http.ResponseWriter, r *http.Request) (tpl, *TemplateData, error) {
   653  	var addressParam string
   654  	i := strings.LastIndexByte(r.URL.Path, '/')
   655  	if i > 0 {
   656  		addressParam = r.URL.Path[i+1:]
   657  	}
   658  	if len(addressParam) == 0 {
   659  		return errorTpl, nil, api.NewAPIError("Missing address", true)
   660  	}
   661  	s.metrics.ExplorerViews.With(common.Labels{"action": "address"}).Inc()
   662  	page, _, _, filter, filterParam, _ := s.getAddressQueryParams(r, api.AccountDetailsTxHistoryLight, txsOnPage)
   663  	// do not allow details to be changed by query params
   664  	address, err := s.api.GetAddress(addressParam, page, txsOnPage, api.AccountDetailsTxHistoryLight, filter)
   665  	if err != nil {
   666  		return errorTpl, nil, err
   667  	}
   668  	data := s.newTemplateData()
   669  	data.AddrStr = address.AddrStr
   670  	data.Address = address
   671  	data.Page = address.Page
   672  	data.PagingRange, data.PrevPage, data.NextPage = getPagingRange(address.Page, address.TotalPages)
   673  	if filterParam != "" {
   674  		data.PageParams = template.URL("&filter=" + filterParam)
   675  		data.Address.Filter = filterParam
   676  	}
   677  	return addressTpl, data, nil
   678  }
   679  
   680  func (s *PublicServer) explorerXpub(w http.ResponseWriter, r *http.Request) (tpl, *TemplateData, error) {
   681  	var xpub string
   682  	i := strings.LastIndexByte(r.URL.Path, '/')
   683  	if i > 0 {
   684  		xpub = r.URL.Path[i+1:]
   685  	}
   686  	if len(xpub) == 0 {
   687  		return errorTpl, nil, api.NewAPIError("Missing xpub", true)
   688  	}
   689  	s.metrics.ExplorerViews.With(common.Labels{"action": "xpub"}).Inc()
   690  	page, _, _, filter, filterParam, gap := s.getAddressQueryParams(r, api.AccountDetailsTxHistoryLight, txsOnPage)
   691  	// do not allow txsOnPage and details to be changed by query params
   692  	address, err := s.api.GetXpubAddress(xpub, page, txsOnPage, api.AccountDetailsTxHistoryLight, filter, gap)
   693  	if err != nil {
   694  		if err == api.ErrUnsupportedXpub {
   695  			err = api.NewAPIError("XPUB functionality is not supported", true)
   696  		}
   697  		return errorTpl, nil, err
   698  	}
   699  	data := s.newTemplateData()
   700  	data.AddrStr = address.AddrStr
   701  	data.Address = address
   702  	data.Page = address.Page
   703  	data.PagingRange, data.PrevPage, data.NextPage = getPagingRange(address.Page, address.TotalPages)
   704  	if filterParam != "" {
   705  		data.PageParams = template.URL("&filter=" + filterParam)
   706  		data.Address.Filter = filterParam
   707  	}
   708  	data.NonZeroBalanceTokens = filter.TokensToReturn == api.TokensToReturnNonzeroBalance
   709  	return xpubTpl, data, nil
   710  }
   711  
   712  func (s *PublicServer) explorerBlocks(w http.ResponseWriter, r *http.Request) (tpl, *TemplateData, error) {
   713  	var blocks *api.Blocks
   714  	var err error
   715  	s.metrics.ExplorerViews.With(common.Labels{"action": "blocks"}).Inc()
   716  	page, ec := strconv.Atoi(r.URL.Query().Get("page"))
   717  	if ec != nil {
   718  		page = 0
   719  	}
   720  	blocks, err = s.api.GetBlocks(page, blocksOnPage)
   721  	if err != nil {
   722  		return errorTpl, nil, err
   723  	}
   724  	data := s.newTemplateData()
   725  	data.Blocks = blocks
   726  	data.Page = blocks.Page
   727  	data.PagingRange, data.PrevPage, data.NextPage = getPagingRange(blocks.Page, blocks.TotalPages)
   728  	return blocksTpl, data, nil
   729  }
   730  
   731  func (s *PublicServer) explorerBlock(w http.ResponseWriter, r *http.Request) (tpl, *TemplateData, error) {
   732  	var block *api.Block
   733  	var err error
   734  	s.metrics.ExplorerViews.With(common.Labels{"action": "block"}).Inc()
   735  	if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 {
   736  		page, ec := strconv.Atoi(r.URL.Query().Get("page"))
   737  		if ec != nil {
   738  			page = 0
   739  		}
   740  		block, err = s.api.GetBlock(r.URL.Path[i+1:], page, txsOnPage)
   741  		if err != nil {
   742  			return errorTpl, nil, err
   743  		}
   744  	}
   745  	data := s.newTemplateData()
   746  	data.Block = block
   747  	data.Page = block.Page
   748  	data.PagingRange, data.PrevPage, data.NextPage = getPagingRange(block.Page, block.TotalPages)
   749  	return blockTpl, data, nil
   750  }
   751  
   752  func (s *PublicServer) explorerIndex(w http.ResponseWriter, r *http.Request) (tpl, *TemplateData, error) {
   753  	var si *api.SystemInfo
   754  	var err error
   755  	s.metrics.ExplorerViews.With(common.Labels{"action": "index"}).Inc()
   756  	si, err = s.api.GetSystemInfo(false)
   757  	if err != nil {
   758  		return errorTpl, nil, err
   759  	}
   760  	data := s.newTemplateData()
   761  	data.Info = si
   762  	return indexTpl, data, nil
   763  }
   764  
   765  func (s *PublicServer) explorerSearch(w http.ResponseWriter, r *http.Request) (tpl, *TemplateData, error) {
   766  	q := strings.TrimSpace(r.URL.Query().Get("q"))
   767  	var tx *api.Tx
   768  	var address *api.Address
   769  	var block *api.Block
   770  	var err error
   771  	s.metrics.ExplorerViews.With(common.Labels{"action": "search"}).Inc()
   772  	if len(q) > 0 {
   773  		address, err = s.api.GetXpubAddress(q, 0, 1, api.AccountDetailsBasic, &api.AddressFilter{Vout: api.AddressFilterVoutOff}, 0)
   774  		if err == nil {
   775  			http.Redirect(w, r, joinURL("/xpub/", address.AddrStr), 302)
   776  			return noTpl, nil, nil
   777  		}
   778  		block, err = s.api.GetBlock(q, 0, 1)
   779  		if err == nil {
   780  			http.Redirect(w, r, joinURL("/block/", block.Hash), 302)
   781  			return noTpl, nil, nil
   782  		}
   783  		tx, err = s.api.GetTransaction(q, false, false)
   784  		if err == nil {
   785  			http.Redirect(w, r, joinURL("/tx/", tx.Txid), 302)
   786  			return noTpl, nil, nil
   787  		}
   788  		address, err = s.api.GetAddress(q, 0, 1, api.AccountDetailsBasic, &api.AddressFilter{Vout: api.AddressFilterVoutOff})
   789  		if err == nil {
   790  			http.Redirect(w, r, joinURL("/address/", address.AddrStr), 302)
   791  			return noTpl, nil, nil
   792  		}
   793  	}
   794  	return errorTpl, nil, api.NewAPIError(fmt.Sprintf("No matching records found for '%v'", q), true)
   795  }
   796  
   797  func (s *PublicServer) explorerSendTx(w http.ResponseWriter, r *http.Request) (tpl, *TemplateData, error) {
   798  	s.metrics.ExplorerViews.With(common.Labels{"action": "sendtx"}).Inc()
   799  	data := s.newTemplateData()
   800  	if r.Method == http.MethodPost {
   801  		err := r.ParseForm()
   802  		if err != nil {
   803  			return sendTransactionTpl, data, err
   804  		}
   805  		hex := r.FormValue("hex")
   806  		if len(hex) > 0 {
   807  			res, err := s.chain.SendRawTransaction(hex)
   808  			if err != nil {
   809  				data.SendTxHex = hex
   810  				data.Error = &api.APIError{Text: err.Error(), Public: true}
   811  				return sendTransactionTpl, data, nil
   812  			}
   813  			data.Status = "Transaction sent, result " + res
   814  		}
   815  	}
   816  	return sendTransactionTpl, data, nil
   817  }
   818  
   819  func (s *PublicServer) explorerMempool(w http.ResponseWriter, r *http.Request) (tpl, *TemplateData, error) {
   820  	var mempoolTxids *api.MempoolTxids
   821  	var err error
   822  	s.metrics.ExplorerViews.With(common.Labels{"action": "mempool"}).Inc()
   823  	page, ec := strconv.Atoi(r.URL.Query().Get("page"))
   824  	if ec != nil {
   825  		page = 0
   826  	}
   827  	mempoolTxids, err = s.api.GetMempool(page, mempoolTxsOnPage)
   828  	if err != nil {
   829  		return errorTpl, nil, err
   830  	}
   831  	data := s.newTemplateData()
   832  	data.MempoolTxids = mempoolTxids
   833  	data.Page = mempoolTxids.Page
   834  	data.PagingRange, data.PrevPage, data.NextPage = getPagingRange(mempoolTxids.Page, mempoolTxids.TotalPages)
   835  	return mempoolTpl, data, nil
   836  }
   837  
   838  func getPagingRange(page int, total int) ([]int, int, int) {
   839  	// total==-1 means total is unknown, show only prev/next buttons
   840  	if total >= 0 && total < 2 {
   841  		return nil, 0, 0
   842  	}
   843  	var r []int
   844  	pp, np := page-1, page+1
   845  	if pp < 1 {
   846  		pp = 1
   847  	}
   848  	if total > 0 {
   849  		if np > total {
   850  			np = total
   851  		}
   852  		r = make([]int, 0, 8)
   853  		if total < 6 {
   854  			for i := 1; i <= total; i++ {
   855  				r = append(r, i)
   856  			}
   857  		} else {
   858  			r = append(r, 1)
   859  			if page > 3 {
   860  				r = append(r, 0)
   861  			}
   862  			if pp == 1 {
   863  				if page == 1 {
   864  					r = append(r, np)
   865  					r = append(r, np+1)
   866  					r = append(r, np+2)
   867  				} else {
   868  					r = append(r, page)
   869  					r = append(r, np)
   870  					r = append(r, np+1)
   871  				}
   872  			} else if np == total {
   873  				if page == total {
   874  					r = append(r, pp-2)
   875  					r = append(r, pp-1)
   876  					r = append(r, pp)
   877  				} else {
   878  					r = append(r, pp-1)
   879  					r = append(r, pp)
   880  					r = append(r, page)
   881  				}
   882  			} else {
   883  				r = append(r, pp)
   884  				r = append(r, page)
   885  				r = append(r, np)
   886  			}
   887  			if page <= total-3 {
   888  				r = append(r, 0)
   889  			}
   890  			r = append(r, total)
   891  		}
   892  	}
   893  	return r, pp, np
   894  }
   895  
   896  func (s *PublicServer) apiIndex(r *http.Request, apiVersion int) (interface{}, error) {
   897  	s.metrics.ExplorerViews.With(common.Labels{"action": "api-index"}).Inc()
   898  	return s.api.GetSystemInfo(false)
   899  }
   900  
   901  func (s *PublicServer) apiBlockIndex(r *http.Request, apiVersion int) (interface{}, error) {
   902  	type resBlockIndex struct {
   903  		BlockHash string `json:"blockHash"`
   904  	}
   905  	var err error
   906  	var hash string
   907  	height := -1
   908  	if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 {
   909  		if h, err := strconv.Atoi(r.URL.Path[i+1:]); err == nil {
   910  			height = h
   911  		}
   912  	}
   913  	if height >= 0 {
   914  		hash, err = s.db.GetBlockHash(uint32(height))
   915  	} else {
   916  		_, hash, err = s.db.GetBestBlock()
   917  	}
   918  	if err != nil {
   919  		glog.Error(err)
   920  		return nil, err
   921  	}
   922  	return resBlockIndex{
   923  		BlockHash: hash,
   924  	}, nil
   925  }
   926  
   927  func (s *PublicServer) apiTx(r *http.Request, apiVersion int) (interface{}, error) {
   928  	var txid string
   929  	i := strings.LastIndexByte(r.URL.Path, '/')
   930  	if i > 0 {
   931  		txid = r.URL.Path[i+1:]
   932  	}
   933  	if len(txid) == 0 {
   934  		return nil, api.NewAPIError("Missing txid", true)
   935  	}
   936  	var tx *api.Tx
   937  	var err error
   938  	s.metrics.ExplorerViews.With(common.Labels{"action": "api-tx"}).Inc()
   939  	spendingTxs := false
   940  	p := r.URL.Query().Get("spending")
   941  	if len(p) > 0 {
   942  		spendingTxs, err = strconv.ParseBool(p)
   943  		if err != nil {
   944  			return nil, api.NewAPIError("Parameter 'spending' cannot be converted to boolean", true)
   945  		}
   946  	}
   947  	tx, err = s.api.GetTransaction(txid, spendingTxs, false)
   948  	if err == nil && apiVersion == apiV1 {
   949  		return s.api.TxToV1(tx), nil
   950  	}
   951  	return tx, err
   952  }
   953  
   954  func (s *PublicServer) apiTxSpecific(r *http.Request, apiVersion int) (interface{}, error) {
   955  	var txid string
   956  	i := strings.LastIndexByte(r.URL.Path, '/')
   957  	if i > 0 {
   958  		txid = r.URL.Path[i+1:]
   959  	}
   960  	if len(txid) == 0 {
   961  		return nil, api.NewAPIError("Missing txid", true)
   962  	}
   963  	var tx json.RawMessage
   964  	var err error
   965  	s.metrics.ExplorerViews.With(common.Labels{"action": "api-tx-specific"}).Inc()
   966  	tx, err = s.chain.GetTransactionSpecific(&bchain.Tx{Txid: txid})
   967  	return tx, err
   968  }
   969  
   970  func (s *PublicServer) apiAddress(r *http.Request, apiVersion int) (interface{}, error) {
   971  	var addressParam string
   972  	i := strings.LastIndexByte(r.URL.Path, '/')
   973  	if i > 0 {
   974  		addressParam = r.URL.Path[i+1:]
   975  	}
   976  	if len(addressParam) == 0 {
   977  		return nil, api.NewAPIError("Missing address", true)
   978  	}
   979  	var address *api.Address
   980  	var err error
   981  	s.metrics.ExplorerViews.With(common.Labels{"action": "api-address"}).Inc()
   982  	page, pageSize, details, filter, _, _ := s.getAddressQueryParams(r, api.AccountDetailsTxidHistory, txsInAPI)
   983  	address, err = s.api.GetAddress(addressParam, page, pageSize, details, filter)
   984  	if err == nil && apiVersion == apiV1 {
   985  		return s.api.AddressToV1(address), nil
   986  	}
   987  	return address, err
   988  }
   989  
   990  func (s *PublicServer) apiXpub(r *http.Request, apiVersion int) (interface{}, error) {
   991  	var xpub string
   992  	i := strings.LastIndexByte(r.URL.Path, '/')
   993  	if i > 0 {
   994  		xpub = r.URL.Path[i+1:]
   995  	}
   996  	if len(xpub) == 0 {
   997  		return nil, api.NewAPIError("Missing xpub", true)
   998  	}
   999  	var address *api.Address
  1000  	var err error
  1001  	s.metrics.ExplorerViews.With(common.Labels{"action": "api-xpub"}).Inc()
  1002  	page, pageSize, details, filter, _, gap := s.getAddressQueryParams(r, api.AccountDetailsTxidHistory, txsInAPI)
  1003  	address, err = s.api.GetXpubAddress(xpub, page, pageSize, details, filter, gap)
  1004  	if err == nil && apiVersion == apiV1 {
  1005  		return s.api.AddressToV1(address), nil
  1006  	}
  1007  	if err == api.ErrUnsupportedXpub {
  1008  		err = api.NewAPIError("XPUB functionality is not supported", true)
  1009  	}
  1010  	return address, err
  1011  }
  1012  
  1013  func (s *PublicServer) apiUtxo(r *http.Request, apiVersion int) (interface{}, error) {
  1014  	var utxo []api.Utxo
  1015  	var err error
  1016  	if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 {
  1017  		onlyConfirmed := false
  1018  		c := r.URL.Query().Get("confirmed")
  1019  		if len(c) > 0 {
  1020  			onlyConfirmed, err = strconv.ParseBool(c)
  1021  			if err != nil {
  1022  				return nil, api.NewAPIError("Parameter 'confirmed' cannot be converted to boolean", true)
  1023  			}
  1024  		}
  1025  		gap, ec := strconv.Atoi(r.URL.Query().Get("gap"))
  1026  		if ec != nil {
  1027  			gap = 0
  1028  		}
  1029  		utxo, err = s.api.GetXpubUtxo(r.URL.Path[i+1:], onlyConfirmed, gap)
  1030  		if err == nil {
  1031  			s.metrics.ExplorerViews.With(common.Labels{"action": "api-xpub-utxo"}).Inc()
  1032  		} else {
  1033  			utxo, err = s.api.GetAddressUtxo(r.URL.Path[i+1:], onlyConfirmed)
  1034  			s.metrics.ExplorerViews.With(common.Labels{"action": "api-address-utxo"}).Inc()
  1035  		}
  1036  		if err == nil && apiVersion == apiV1 {
  1037  			return s.api.AddressUtxoToV1(utxo), nil
  1038  		}
  1039  	}
  1040  	return utxo, err
  1041  }
  1042  
  1043  func (s *PublicServer) apiBalanceHistory(r *http.Request, apiVersion int) (interface{}, error) {
  1044  	var history []api.BalanceHistory
  1045  	var fromTimestamp, toTimestamp int64
  1046  	var err error
  1047  	if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 {
  1048  		gap, ec := strconv.Atoi(r.URL.Query().Get("gap"))
  1049  		if ec != nil {
  1050  			gap = 0
  1051  		}
  1052  		from := r.URL.Query().Get("from")
  1053  		if from != "" {
  1054  			fromTimestamp, err = strconv.ParseInt(from, 10, 64)
  1055  			if err != nil {
  1056  				return history, err
  1057  			}
  1058  		}
  1059  		to := r.URL.Query().Get("to")
  1060  		if to != "" {
  1061  			toTimestamp, err = strconv.ParseInt(to, 10, 64)
  1062  			if err != nil {
  1063  				return history, err
  1064  			}
  1065  		}
  1066  		var groupBy uint64
  1067  		groupBy, err = strconv.ParseUint(r.URL.Query().Get("groupBy"), 10, 32)
  1068  		if err != nil || groupBy == 0 {
  1069  			groupBy = 3600
  1070  		}
  1071  		fiat := r.URL.Query().Get("fiatcurrency")
  1072  		var fiatArray []string
  1073  		if fiat != "" {
  1074  			fiatArray = []string{fiat}
  1075  		}
  1076  		history, err = s.api.GetXpubBalanceHistory(r.URL.Path[i+1:], fromTimestamp, toTimestamp, fiatArray, gap, uint32(groupBy))
  1077  		if err == nil {
  1078  			s.metrics.ExplorerViews.With(common.Labels{"action": "api-xpub-balancehistory"}).Inc()
  1079  		} else {
  1080  			history, err = s.api.GetBalanceHistory(r.URL.Path[i+1:], fromTimestamp, toTimestamp, fiatArray, uint32(groupBy))
  1081  			s.metrics.ExplorerViews.With(common.Labels{"action": "api-address-balancehistory"}).Inc()
  1082  		}
  1083  	}
  1084  	return history, err
  1085  }
  1086  
  1087  func (s *PublicServer) apiBlock(r *http.Request, apiVersion int) (interface{}, error) {
  1088  	var block *api.Block
  1089  	var err error
  1090  	s.metrics.ExplorerViews.With(common.Labels{"action": "api-block"}).Inc()
  1091  	if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 {
  1092  		page, ec := strconv.Atoi(r.URL.Query().Get("page"))
  1093  		if ec != nil {
  1094  			page = 0
  1095  		}
  1096  		block, err = s.api.GetBlock(r.URL.Path[i+1:], page, txsInAPI)
  1097  		if err == nil && apiVersion == apiV1 {
  1098  			return s.api.BlockToV1(block), nil
  1099  		}
  1100  	}
  1101  	return block, err
  1102  }
  1103  
  1104  func (s *PublicServer) apiFeeStats(r *http.Request, apiVersion int) (interface{}, error) {
  1105  	var feeStats *api.FeeStats
  1106  	var err error
  1107  	s.metrics.ExplorerViews.With(common.Labels{"action": "api-feestats"}).Inc()
  1108  	if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 {
  1109  		feeStats, err = s.api.GetFeeStats(r.URL.Path[i+1:])
  1110  	}
  1111  	return feeStats, err
  1112  }
  1113  
  1114  type resultSendTransaction struct {
  1115  	Result string `json:"result"`
  1116  }
  1117  
  1118  func (s *PublicServer) apiSendTx(r *http.Request, apiVersion int) (interface{}, error) {
  1119  	var err error
  1120  	var res resultSendTransaction
  1121  	var hex string
  1122  	s.metrics.ExplorerViews.With(common.Labels{"action": "api-sendtx"}).Inc()
  1123  	if r.Method == http.MethodPost {
  1124  		data, err := ioutil.ReadAll(r.Body)
  1125  		if err != nil {
  1126  			return nil, api.NewAPIError("Missing tx blob", true)
  1127  		}
  1128  		hex = string(data)
  1129  	} else {
  1130  		if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 {
  1131  			hex = r.URL.Path[i+1:]
  1132  		}
  1133  	}
  1134  	if len(hex) > 0 {
  1135  		res.Result, err = s.chain.SendRawTransaction(hex)
  1136  		if err != nil {
  1137  			return nil, api.NewAPIError(err.Error(), true)
  1138  		}
  1139  		return res, nil
  1140  	}
  1141  	return nil, api.NewAPIError("Missing tx blob", true)
  1142  }
  1143  
  1144  // apiTickersList returns a list of available FiatRates currencies
  1145  func (s *PublicServer) apiTickersList(r *http.Request, apiVersion int) (interface{}, error) {
  1146  	s.metrics.ExplorerViews.With(common.Labels{"action": "api-tickers-list"}).Inc()
  1147  	timestampString := strings.ToLower(r.URL.Query().Get("timestamp"))
  1148  	timestamp, err := strconv.ParseInt(timestampString, 10, 64)
  1149  	if err != nil {
  1150  		return nil, api.NewAPIError("Parameter \"timestamp\" is not a valid Unix timestamp.", true)
  1151  	}
  1152  	result, err := s.api.GetFiatRatesTickersList(timestamp)
  1153  	return result, err
  1154  }
  1155  
  1156  // apiTickers returns FiatRates ticker prices for the specified block or timestamp.
  1157  func (s *PublicServer) apiTickers(r *http.Request, apiVersion int) (interface{}, error) {
  1158  	var result *db.ResultTickerAsString
  1159  	var err error
  1160  
  1161  	currency := strings.ToLower(r.URL.Query().Get("currency"))
  1162  	var currencies []string
  1163  	if currency != "" {
  1164  		currencies = []string{currency}
  1165  	}
  1166  
  1167  	if block := r.URL.Query().Get("block"); block != "" {
  1168  		// Get tickers for specified block height or block hash
  1169  		s.metrics.ExplorerViews.With(common.Labels{"action": "api-tickers-block"}).Inc()
  1170  		result, err = s.api.GetFiatRatesForBlockID(block, currencies)
  1171  	} else if timestampString := r.URL.Query().Get("timestamp"); timestampString != "" {
  1172  		// Get tickers for specified timestamp
  1173  		s.metrics.ExplorerViews.With(common.Labels{"action": "api-tickers-date"}).Inc()
  1174  
  1175  		timestamp, err := strconv.ParseInt(timestampString, 10, 64)
  1176  		if err != nil {
  1177  			return nil, api.NewAPIError("Parameter \"timestamp\" is not a valid Unix timestamp.", true)
  1178  		}
  1179  
  1180  		resultTickers, err := s.api.GetFiatRatesForTimestamps([]int64{timestamp}, currencies)
  1181  		if err != nil {
  1182  			return nil, err
  1183  		}
  1184  		result = &resultTickers.Tickers[0]
  1185  	} else {
  1186  		// No parameters - get the latest available ticker
  1187  		s.metrics.ExplorerViews.With(common.Labels{"action": "api-tickers-last"}).Inc()
  1188  		result, err = s.api.GetCurrentFiatRates(currencies)
  1189  	}
  1190  	if err != nil {
  1191  		return nil, err
  1192  	}
  1193  	return result, nil
  1194  }
  1195  
  1196  type resultEstimateFeeAsString struct {
  1197  	Result string `json:"result"`
  1198  }
  1199  
  1200  func (s *PublicServer) apiEstimateFee(r *http.Request, apiVersion int) (interface{}, error) {
  1201  	var res resultEstimateFeeAsString
  1202  	s.metrics.ExplorerViews.With(common.Labels{"action": "api-estimatefee"}).Inc()
  1203  	if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 {
  1204  		b := r.URL.Path[i+1:]
  1205  		if len(b) > 0 {
  1206  			blocks, err := strconv.Atoi(b)
  1207  			if err != nil {
  1208  				return nil, api.NewAPIError("Parameter 'number of blocks' is not a number", true)
  1209  			}
  1210  			conservative := true
  1211  			c := r.URL.Query().Get("conservative")
  1212  			if len(c) > 0 {
  1213  				conservative, err = strconv.ParseBool(c)
  1214  				if err != nil {
  1215  					return nil, api.NewAPIError("Parameter 'conservative' cannot be converted to boolean", true)
  1216  				}
  1217  			}
  1218  			var fee big.Int
  1219  			fee, err = s.chain.EstimateSmartFee(blocks, conservative)
  1220  			if err != nil {
  1221  				fee, err = s.chain.EstimateFee(blocks)
  1222  				if err != nil {
  1223  					return nil, err
  1224  				}
  1225  			}
  1226  			res.Result = s.chainParser.AmountToDecimalString(&fee)
  1227  			return res, nil
  1228  		}
  1229  	}
  1230  	return nil, api.NewAPIError("Missing parameter 'number of blocks'", true)
  1231  }