github.com/grupokindynos/coins-explorer@v0.0.0-20210507172551-fa8983d19250/server/public.go (about)

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