1243 lines
40 KiB
Go
1243 lines
40 KiB
Go
package server
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"html/template"
|
|
"io/ioutil"
|
|
"math/big"
|
|
"net/http"
|
|
"path/filepath"
|
|
"reflect"
|
|
"regexp"
|
|
"runtime"
|
|
"runtime/debug"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/golang/glog"
|
|
"github.com/trezor/blockbook/api"
|
|
"github.com/trezor/blockbook/bchain"
|
|
"github.com/trezor/blockbook/common"
|
|
"github.com/trezor/blockbook/db"
|
|
)
|
|
|
|
const txsOnPage = 25
|
|
const blocksOnPage = 50
|
|
const mempoolTxsOnPage = 50
|
|
const txsInAPI = 1000
|
|
|
|
const (
|
|
_ = iota
|
|
apiV1
|
|
apiV2
|
|
)
|
|
|
|
// PublicServer is a handle to public http server
|
|
type PublicServer struct {
|
|
binding string
|
|
certFiles string
|
|
socketio *SocketIoServer
|
|
websocket *WebsocketServer
|
|
https *http.Server
|
|
db *db.RocksDB
|
|
txCache *db.TxCache
|
|
chain bchain.BlockChain
|
|
chainParser bchain.BlockChainParser
|
|
mempool bchain.Mempool
|
|
api *api.Worker
|
|
explorerURL string
|
|
internalExplorer bool
|
|
metrics *common.Metrics
|
|
is *common.InternalState
|
|
templates []*template.Template
|
|
debug bool
|
|
}
|
|
|
|
// NewPublicServer creates new public server http interface to blockbook and returns its handle
|
|
// only basic functionality is mapped, to map all functions, call
|
|
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) {
|
|
|
|
api, err := api.NewWorker(db, chain, mempool, txCache, is)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
socketio, err := NewSocketIoServer(db, chain, mempool, txCache, metrics, is)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
websocket, err := NewWebsocketServer(db, chain, mempool, txCache, metrics, is)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
addr, path := splitBinding(binding)
|
|
serveMux := http.NewServeMux()
|
|
https := &http.Server{
|
|
Addr: addr,
|
|
Handler: serveMux,
|
|
}
|
|
|
|
s := &PublicServer{
|
|
binding: binding,
|
|
certFiles: certFiles,
|
|
https: https,
|
|
api: api,
|
|
socketio: socketio,
|
|
websocket: websocket,
|
|
db: db,
|
|
txCache: txCache,
|
|
chain: chain,
|
|
chainParser: chain.GetChainParser(),
|
|
mempool: mempool,
|
|
explorerURL: explorerURL,
|
|
internalExplorer: explorerURL == "",
|
|
metrics: metrics,
|
|
is: is,
|
|
debug: debugMode,
|
|
}
|
|
s.templates = s.parseTemplates()
|
|
|
|
// map only basic functions, the rest is enabled by method MapFullPublicInterface
|
|
serveMux.Handle(path+"favicon.ico", http.FileServer(http.Dir("./static/")))
|
|
serveMux.Handle(path+"static/", http.StripPrefix("/static/", http.FileServer(http.Dir("./static/"))))
|
|
// default handler
|
|
serveMux.HandleFunc(path, s.htmlTemplateHandler(s.explorerIndex))
|
|
// default API handler
|
|
serveMux.HandleFunc(path+"api/", s.jsonHandler(s.apiIndex, apiV2))
|
|
|
|
return s, nil
|
|
}
|
|
|
|
// Run starts the server
|
|
func (s *PublicServer) Run() error {
|
|
if s.certFiles == "" {
|
|
glog.Info("public server: starting to listen on http://", s.https.Addr)
|
|
return s.https.ListenAndServe()
|
|
}
|
|
glog.Info("public server starting to listen on https://", s.https.Addr)
|
|
return s.https.ListenAndServeTLS(fmt.Sprint(s.certFiles, ".crt"), fmt.Sprint(s.certFiles, ".key"))
|
|
}
|
|
|
|
// ConnectFullPublicInterface enables complete public functionality
|
|
func (s *PublicServer) ConnectFullPublicInterface() {
|
|
serveMux := s.https.Handler.(*http.ServeMux)
|
|
_, path := splitBinding(s.binding)
|
|
// support for test pages
|
|
serveMux.Handle(path+"test-socketio.html", http.FileServer(http.Dir("./static/")))
|
|
serveMux.Handle(path+"test-websocket.html", http.FileServer(http.Dir("./static/")))
|
|
if s.internalExplorer {
|
|
// internal explorer handlers
|
|
serveMux.HandleFunc(path+"tx/", s.htmlTemplateHandler(s.explorerTx))
|
|
serveMux.HandleFunc(path+"address/", s.htmlTemplateHandler(s.explorerAddress))
|
|
serveMux.HandleFunc(path+"xpub/", s.htmlTemplateHandler(s.explorerXpub))
|
|
serveMux.HandleFunc(path+"search/", s.htmlTemplateHandler(s.explorerSearch))
|
|
serveMux.HandleFunc(path+"blocks", s.htmlTemplateHandler(s.explorerBlocks))
|
|
serveMux.HandleFunc(path+"block/", s.htmlTemplateHandler(s.explorerBlock))
|
|
serveMux.HandleFunc(path+"spending/", s.htmlTemplateHandler(s.explorerSpendingTx))
|
|
serveMux.HandleFunc(path+"sendtx", s.htmlTemplateHandler(s.explorerSendTx))
|
|
serveMux.HandleFunc(path+"mempool", s.htmlTemplateHandler(s.explorerMempool))
|
|
} else {
|
|
// redirect to wallet requests for tx and address, possibly to external site
|
|
serveMux.HandleFunc(path+"tx/", s.txRedirect)
|
|
serveMux.HandleFunc(path+"address/", s.addressRedirect)
|
|
}
|
|
// API calls
|
|
// default api without version can be changed to different version at any time
|
|
// use versioned api for stability
|
|
|
|
var apiDefault int
|
|
// ethereum supports only api V2
|
|
if s.chainParser.GetChainType() == bchain.ChainEthereumType {
|
|
apiDefault = apiV2
|
|
} else {
|
|
apiDefault = apiV1
|
|
// legacy v1 format
|
|
serveMux.HandleFunc(path+"api/v1/block-index/", s.jsonHandler(s.apiBlockIndex, apiV1))
|
|
serveMux.HandleFunc(path+"api/v1/tx-specific/", s.jsonHandler(s.apiTxSpecific, apiV1))
|
|
serveMux.HandleFunc(path+"api/v1/tx/", s.jsonHandler(s.apiTx, apiV1))
|
|
serveMux.HandleFunc(path+"api/v1/address/", s.jsonHandler(s.apiAddress, apiV1))
|
|
serveMux.HandleFunc(path+"api/v1/utxo/", s.jsonHandler(s.apiUtxo, apiV1))
|
|
serveMux.HandleFunc(path+"api/v1/block/", s.jsonHandler(s.apiBlock, apiV1))
|
|
serveMux.HandleFunc(path+"api/v1/sendtx/", s.jsonHandler(s.apiSendTx, apiV1))
|
|
serveMux.HandleFunc(path+"api/v1/estimatefee/", s.jsonHandler(s.apiEstimateFee, apiV1))
|
|
}
|
|
serveMux.HandleFunc(path+"api/block-index/", s.jsonHandler(s.apiBlockIndex, apiDefault))
|
|
serveMux.HandleFunc(path+"api/tx-specific/", s.jsonHandler(s.apiTxSpecific, apiDefault))
|
|
serveMux.HandleFunc(path+"api/tx/", s.jsonHandler(s.apiTx, apiDefault))
|
|
serveMux.HandleFunc(path+"api/address/", s.jsonHandler(s.apiAddress, apiDefault))
|
|
serveMux.HandleFunc(path+"api/xpub/", s.jsonHandler(s.apiXpub, apiDefault))
|
|
serveMux.HandleFunc(path+"api/utxo/", s.jsonHandler(s.apiUtxo, apiDefault))
|
|
serveMux.HandleFunc(path+"api/block/", s.jsonHandler(s.apiBlock, apiDefault))
|
|
serveMux.HandleFunc(path+"api/sendtx/", s.jsonHandler(s.apiSendTx, apiDefault))
|
|
serveMux.HandleFunc(path+"api/estimatefee/", s.jsonHandler(s.apiEstimateFee, apiDefault))
|
|
serveMux.HandleFunc(path+"api/balancehistory/", s.jsonHandler(s.apiBalanceHistory, apiDefault))
|
|
// v2 format
|
|
serveMux.HandleFunc(path+"api/v2/block-index/", s.jsonHandler(s.apiBlockIndex, apiV2))
|
|
serveMux.HandleFunc(path+"api/v2/tx-specific/", s.jsonHandler(s.apiTxSpecific, apiV2))
|
|
serveMux.HandleFunc(path+"api/v2/tx/", s.jsonHandler(s.apiTx, apiV2))
|
|
serveMux.HandleFunc(path+"api/v2/address/", s.jsonHandler(s.apiAddress, apiV2))
|
|
serveMux.HandleFunc(path+"api/v2/xpub/", s.jsonHandler(s.apiXpub, apiV2))
|
|
serveMux.HandleFunc(path+"api/v2/utxo/", s.jsonHandler(s.apiUtxo, apiV2))
|
|
serveMux.HandleFunc(path+"api/v2/block/", s.jsonHandler(s.apiBlock, apiV2))
|
|
serveMux.HandleFunc(path+"api/v2/sendtx/", s.jsonHandler(s.apiSendTx, apiV2))
|
|
serveMux.HandleFunc(path+"api/v2/estimatefee/", s.jsonHandler(s.apiEstimateFee, apiV2))
|
|
serveMux.HandleFunc(path+"api/v2/feestats/", s.jsonHandler(s.apiFeeStats, apiV2))
|
|
serveMux.HandleFunc(path+"api/v2/balancehistory/", s.jsonHandler(s.apiBalanceHistory, apiDefault))
|
|
serveMux.HandleFunc(path+"api/v2/tickers/", s.jsonHandler(s.apiTickers, apiV2))
|
|
serveMux.HandleFunc(path+"api/v2/tickers-list/", s.jsonHandler(s.apiTickersList, apiV2))
|
|
// socket.io interface
|
|
serveMux.Handle(path+"socket.io/", s.socketio.GetHandler())
|
|
// websocket interface
|
|
serveMux.Handle(path+"websocket", s.websocket.GetHandler())
|
|
}
|
|
|
|
// Close closes the server
|
|
func (s *PublicServer) Close() error {
|
|
glog.Infof("public server: closing")
|
|
return s.https.Close()
|
|
}
|
|
|
|
// Shutdown shuts down the server
|
|
func (s *PublicServer) Shutdown(ctx context.Context) error {
|
|
glog.Infof("public server: shutdown")
|
|
return s.https.Shutdown(ctx)
|
|
}
|
|
|
|
// OnNewBlock notifies users subscribed to bitcoind/hashblock about new block
|
|
func (s *PublicServer) OnNewBlock(hash string, height uint32) {
|
|
s.socketio.OnNewBlockHash(hash)
|
|
s.websocket.OnNewBlock(hash, height)
|
|
}
|
|
|
|
// OnNewFiatRatesTicker notifies users subscribed to bitcoind/fiatrates about new ticker
|
|
func (s *PublicServer) OnNewFiatRatesTicker(ticker *db.CurrencyRatesTicker) {
|
|
s.websocket.OnNewFiatRatesTicker(ticker)
|
|
}
|
|
|
|
// OnNewTxAddr notifies users subscribed to notification about new tx
|
|
func (s *PublicServer) OnNewTxAddr(tx *bchain.Tx, desc bchain.AddressDescriptor) {
|
|
s.socketio.OnNewTxAddr(tx.Txid, desc)
|
|
}
|
|
|
|
// OnNewTx notifies users subscribed to notification about new tx
|
|
func (s *PublicServer) OnNewTx(tx *bchain.MempoolTx) {
|
|
s.websocket.OnNewTx(tx)
|
|
}
|
|
|
|
func (s *PublicServer) txRedirect(w http.ResponseWriter, r *http.Request) {
|
|
http.Redirect(w, r, joinURL(s.explorerURL, r.URL.Path), 302)
|
|
s.metrics.ExplorerViews.With(common.Labels{"action": "tx-redirect"}).Inc()
|
|
}
|
|
|
|
func (s *PublicServer) addressRedirect(w http.ResponseWriter, r *http.Request) {
|
|
http.Redirect(w, r, joinURL(s.explorerURL, r.URL.Path), 302)
|
|
s.metrics.ExplorerViews.With(common.Labels{"action": "address-redirect"}).Inc()
|
|
}
|
|
|
|
func splitBinding(binding string) (addr string, path string) {
|
|
i := strings.Index(binding, "/")
|
|
if i >= 0 {
|
|
return binding[0:i], binding[i:]
|
|
}
|
|
return binding, "/"
|
|
}
|
|
|
|
func joinURL(base string, part string) string {
|
|
if len(base) > 0 {
|
|
if len(base) > 0 && base[len(base)-1] == '/' && len(part) > 0 && part[0] == '/' {
|
|
return base + part[1:]
|
|
}
|
|
return base + part
|
|
}
|
|
return part
|
|
}
|
|
|
|
func getFunctionName(i interface{}) string {
|
|
return runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name()
|
|
}
|
|
|
|
func (s *PublicServer) jsonHandler(handler func(r *http.Request, apiVersion int) (interface{}, error), apiVersion int) func(w http.ResponseWriter, r *http.Request) {
|
|
type jsonError struct {
|
|
Text string `json:"error"`
|
|
HTTPStatus int `json:"-"`
|
|
}
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
var data interface{}
|
|
var err error
|
|
defer func() {
|
|
if e := recover(); e != nil {
|
|
glog.Error(getFunctionName(handler), " recovered from panic: ", e)
|
|
debug.PrintStack()
|
|
if s.debug {
|
|
data = jsonError{fmt.Sprint("Internal server error: recovered from panic ", e), http.StatusInternalServerError}
|
|
} else {
|
|
data = jsonError{"Internal server error", http.StatusInternalServerError}
|
|
}
|
|
}
|
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
if e, isError := data.(jsonError); isError {
|
|
w.WriteHeader(e.HTTPStatus)
|
|
}
|
|
err = json.NewEncoder(w).Encode(data)
|
|
if err != nil {
|
|
glog.Warning("json encode ", err)
|
|
}
|
|
}()
|
|
data, err = handler(r, apiVersion)
|
|
if err != nil || data == nil {
|
|
if apiErr, ok := err.(*api.APIError); ok {
|
|
if apiErr.Public {
|
|
data = jsonError{apiErr.Error(), http.StatusBadRequest}
|
|
} else {
|
|
data = jsonError{apiErr.Error(), http.StatusInternalServerError}
|
|
}
|
|
} else {
|
|
if err != nil {
|
|
glog.Error(getFunctionName(handler), " error: ", err)
|
|
}
|
|
if s.debug {
|
|
if data != nil {
|
|
data = jsonError{fmt.Sprintf("Internal server error: %v, data %+v", err, data), http.StatusInternalServerError}
|
|
} else {
|
|
data = jsonError{fmt.Sprintf("Internal server error: %v", err), http.StatusInternalServerError}
|
|
}
|
|
} else {
|
|
data = jsonError{"Internal server error", http.StatusInternalServerError}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *PublicServer) newTemplateData() *TemplateData {
|
|
return &TemplateData{
|
|
CoinName: s.is.Coin,
|
|
CoinShortcut: s.is.CoinShortcut,
|
|
CoinLabel: s.is.CoinLabel,
|
|
ChainType: s.chainParser.GetChainType(),
|
|
InternalExplorer: s.internalExplorer && !s.is.InitialSync,
|
|
TOSLink: api.Text.TOSLink,
|
|
}
|
|
}
|
|
|
|
func (s *PublicServer) newTemplateDataWithError(text string) *TemplateData {
|
|
td := s.newTemplateData()
|
|
td.Error = &api.APIError{Text: text}
|
|
return td
|
|
}
|
|
|
|
func (s *PublicServer) htmlTemplateHandler(handler func(w http.ResponseWriter, r *http.Request) (tpl, *TemplateData, error)) func(w http.ResponseWriter, r *http.Request) {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
var t tpl
|
|
var data *TemplateData
|
|
var err error
|
|
defer func() {
|
|
if e := recover(); e != nil {
|
|
glog.Error(getFunctionName(handler), " recovered from panic: ", e)
|
|
debug.PrintStack()
|
|
t = errorInternalTpl
|
|
if s.debug {
|
|
data = s.newTemplateDataWithError(fmt.Sprint("Internal server error: recovered from panic ", e))
|
|
} else {
|
|
data = s.newTemplateDataWithError("Internal server error")
|
|
}
|
|
}
|
|
// noTpl means the handler completely handled the request
|
|
if t != noTpl {
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
// return 500 Internal Server Error with errorInternalTpl
|
|
if t == errorInternalTpl {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
}
|
|
if err := s.templates[t].ExecuteTemplate(w, "base.html", data); err != nil {
|
|
glog.Error(err)
|
|
}
|
|
}
|
|
}()
|
|
if s.debug {
|
|
// reload templates on each request
|
|
// to reflect changes during development
|
|
s.templates = s.parseTemplates()
|
|
}
|
|
t, data, err = handler(w, r)
|
|
if err != nil || (data == nil && t != noTpl) {
|
|
t = errorInternalTpl
|
|
if apiErr, ok := err.(*api.APIError); ok {
|
|
data = s.newTemplateData()
|
|
data.Error = apiErr
|
|
if apiErr.Public {
|
|
t = errorTpl
|
|
}
|
|
} else {
|
|
if err != nil {
|
|
glog.Error(getFunctionName(handler), " error: ", err)
|
|
}
|
|
if s.debug {
|
|
data = s.newTemplateDataWithError(fmt.Sprintf("Internal server error: %v, data %+v", err, data))
|
|
} else {
|
|
data = s.newTemplateDataWithError("Internal server error")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
type tpl int
|
|
|
|
const (
|
|
noTpl = tpl(iota)
|
|
errorTpl
|
|
errorInternalTpl
|
|
indexTpl
|
|
txTpl
|
|
addressTpl
|
|
xpubTpl
|
|
blocksTpl
|
|
blockTpl
|
|
sendTransactionTpl
|
|
mempoolTpl
|
|
|
|
tplCount
|
|
)
|
|
|
|
// TemplateData is used to transfer data to the templates
|
|
type TemplateData struct {
|
|
CoinName string
|
|
CoinShortcut string
|
|
CoinLabel string
|
|
InternalExplorer bool
|
|
ChainType bchain.ChainType
|
|
Address *api.Address
|
|
AddrStr string
|
|
Tx *api.Tx
|
|
Error *api.APIError
|
|
Blocks *api.Blocks
|
|
Block *api.Block
|
|
Info *api.SystemInfo
|
|
MempoolTxids *api.MempoolTxids
|
|
Page int
|
|
PrevPage int
|
|
NextPage int
|
|
PagingRange []int
|
|
PageParams template.URL
|
|
TOSLink string
|
|
SendTxHex string
|
|
Status string
|
|
NonZeroBalanceTokens bool
|
|
}
|
|
|
|
func (s *PublicServer) parseTemplates() []*template.Template {
|
|
templateFuncMap := template.FuncMap{
|
|
"formatTime": formatTime,
|
|
"formatUnixTime": formatUnixTime,
|
|
"formatAmount": s.formatAmount,
|
|
"formatAmountWithDecimals": formatAmountWithDecimals,
|
|
"setTxToTemplateData": setTxToTemplateData,
|
|
"isOwnAddress": isOwnAddress,
|
|
"isOwnAddresses": isOwnAddresses,
|
|
}
|
|
var createTemplate func(filenames ...string) *template.Template
|
|
if s.debug {
|
|
createTemplate = func(filenames ...string) *template.Template {
|
|
if len(filenames) == 0 {
|
|
panic("Missing templates")
|
|
}
|
|
return template.Must(template.New(filepath.Base(filenames[0])).Funcs(templateFuncMap).ParseFiles(filenames...))
|
|
}
|
|
} else {
|
|
createTemplate = func(filenames ...string) *template.Template {
|
|
if len(filenames) == 0 {
|
|
panic("Missing templates")
|
|
}
|
|
t := template.New(filepath.Base(filenames[0])).Funcs(templateFuncMap)
|
|
for _, filename := range filenames {
|
|
b, err := ioutil.ReadFile(filename)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
// perform very simple minification - replace leading spaces used as formatting and new lines
|
|
r := regexp.MustCompile(`\n\s*`)
|
|
b = r.ReplaceAll(b, []byte{})
|
|
s := string(b)
|
|
name := filepath.Base(filename)
|
|
var tt *template.Template
|
|
if name == t.Name() {
|
|
tt = t
|
|
} else {
|
|
tt = t.New(name)
|
|
}
|
|
_, err = tt.Parse(s)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
return t
|
|
}
|
|
}
|
|
t := make([]*template.Template, tplCount)
|
|
t[errorTpl] = createTemplate("./static/templates/error.html", "./static/templates/base.html")
|
|
t[errorInternalTpl] = createTemplate("./static/templates/error.html", "./static/templates/base.html")
|
|
t[indexTpl] = createTemplate("./static/templates/index.html", "./static/templates/base.html")
|
|
t[blocksTpl] = createTemplate("./static/templates/blocks.html", "./static/templates/paging.html", "./static/templates/base.html")
|
|
t[sendTransactionTpl] = createTemplate("./static/templates/sendtx.html", "./static/templates/base.html")
|
|
if s.chainParser.GetChainType() == bchain.ChainEthereumType {
|
|
t[txTpl] = createTemplate("./static/templates/tx.html", "./static/templates/txdetail_ethereumtype.html", "./static/templates/base.html")
|
|
t[addressTpl] = createTemplate("./static/templates/address.html", "./static/templates/txdetail_ethereumtype.html", "./static/templates/paging.html", "./static/templates/base.html")
|
|
t[blockTpl] = createTemplate("./static/templates/block.html", "./static/templates/txdetail_ethereumtype.html", "./static/templates/paging.html", "./static/templates/base.html")
|
|
} else {
|
|
t[txTpl] = createTemplate("./static/templates/tx.html", "./static/templates/txdetail.html", "./static/templates/base.html")
|
|
t[addressTpl] = createTemplate("./static/templates/address.html", "./static/templates/txdetail.html", "./static/templates/paging.html", "./static/templates/base.html")
|
|
t[blockTpl] = createTemplate("./static/templates/block.html", "./static/templates/txdetail.html", "./static/templates/paging.html", "./static/templates/base.html")
|
|
}
|
|
t[xpubTpl] = createTemplate("./static/templates/xpub.html", "./static/templates/txdetail.html", "./static/templates/paging.html", "./static/templates/base.html")
|
|
t[mempoolTpl] = createTemplate("./static/templates/mempool.html", "./static/templates/paging.html", "./static/templates/base.html")
|
|
return t
|
|
}
|
|
|
|
func formatUnixTime(ut int64) string {
|
|
return formatTime(time.Unix(ut, 0))
|
|
}
|
|
|
|
func formatTime(t time.Time) string {
|
|
return t.Format(time.RFC1123)
|
|
}
|
|
|
|
// for now return the string as it is
|
|
// in future could be used to do coin specific formatting
|
|
func (s *PublicServer) formatAmount(a *api.Amount) string {
|
|
if a == nil {
|
|
return "0"
|
|
}
|
|
return s.chainParser.AmountToDecimalString((*big.Int)(a))
|
|
}
|
|
|
|
func formatAmountWithDecimals(a *api.Amount, d int) string {
|
|
if a == nil {
|
|
return "0"
|
|
}
|
|
return a.DecimalString(d)
|
|
}
|
|
|
|
// called from template to support txdetail.html functionality
|
|
func setTxToTemplateData(td *TemplateData, tx *api.Tx) *TemplateData {
|
|
td.Tx = tx
|
|
return td
|
|
}
|
|
|
|
// returns true if address is "own",
|
|
// i.e. either the address of the address detail or belonging to the xpub
|
|
func isOwnAddress(td *TemplateData, a string) bool {
|
|
if a == td.AddrStr {
|
|
return true
|
|
}
|
|
if td.Address != nil && td.Address.XPubAddresses != nil {
|
|
if _, found := td.Address.XPubAddresses[a]; found {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// returns true if addresses are "own",
|
|
// i.e. either the address of the address detail or belonging to the xpub
|
|
func isOwnAddresses(td *TemplateData, addresses []string) bool {
|
|
if len(addresses) == 1 {
|
|
return isOwnAddress(td, addresses[0])
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (s *PublicServer) explorerTx(w http.ResponseWriter, r *http.Request) (tpl, *TemplateData, error) {
|
|
var tx *api.Tx
|
|
var err error
|
|
s.metrics.ExplorerViews.With(common.Labels{"action": "tx"}).Inc()
|
|
if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 {
|
|
txid := r.URL.Path[i+1:]
|
|
tx, err = s.api.GetTransaction(txid, false, true)
|
|
if err != nil {
|
|
return errorTpl, nil, err
|
|
}
|
|
}
|
|
data := s.newTemplateData()
|
|
data.Tx = tx
|
|
return txTpl, data, nil
|
|
}
|
|
|
|
func (s *PublicServer) explorerSpendingTx(w http.ResponseWriter, r *http.Request) (tpl, *TemplateData, error) {
|
|
s.metrics.ExplorerViews.With(common.Labels{"action": "spendingtx"}).Inc()
|
|
var err error
|
|
parts := strings.Split(r.URL.Path, "/")
|
|
if len(parts) > 2 {
|
|
tx := parts[len(parts)-2]
|
|
n, ec := strconv.Atoi(parts[len(parts)-1])
|
|
if ec == nil {
|
|
spendingTx, err := s.api.GetSpendingTxid(tx, n)
|
|
if err == nil && spendingTx != "" {
|
|
http.Redirect(w, r, joinURL("/tx/", spendingTx), 302)
|
|
return noTpl, nil, nil
|
|
}
|
|
}
|
|
}
|
|
if err == nil {
|
|
err = api.NewAPIError("Transaction not found", true)
|
|
}
|
|
return errorTpl, nil, err
|
|
}
|
|
|
|
func (s *PublicServer) getAddressQueryParams(r *http.Request, accountDetails api.AccountDetails, maxPageSize int) (int, int, api.AccountDetails, *api.AddressFilter, string, int) {
|
|
var voutFilter = api.AddressFilterVoutOff
|
|
page, ec := strconv.Atoi(r.URL.Query().Get("page"))
|
|
if ec != nil {
|
|
page = 0
|
|
}
|
|
pageSize, ec := strconv.Atoi(r.URL.Query().Get("pageSize"))
|
|
if ec != nil || pageSize > maxPageSize {
|
|
pageSize = maxPageSize
|
|
}
|
|
from, ec := strconv.Atoi(r.URL.Query().Get("from"))
|
|
if ec != nil {
|
|
from = 0
|
|
}
|
|
to, ec := strconv.Atoi(r.URL.Query().Get("to"))
|
|
if ec != nil {
|
|
to = 0
|
|
}
|
|
filterParam := r.URL.Query().Get("filter")
|
|
if len(filterParam) > 0 {
|
|
if filterParam == "inputs" {
|
|
voutFilter = api.AddressFilterVoutInputs
|
|
} else if filterParam == "outputs" {
|
|
voutFilter = api.AddressFilterVoutOutputs
|
|
} else {
|
|
voutFilter, ec = strconv.Atoi(filterParam)
|
|
if ec != nil || voutFilter < 0 {
|
|
voutFilter = api.AddressFilterVoutOff
|
|
}
|
|
}
|
|
}
|
|
switch r.URL.Query().Get("details") {
|
|
case "basic":
|
|
accountDetails = api.AccountDetailsBasic
|
|
case "tokens":
|
|
accountDetails = api.AccountDetailsTokens
|
|
case "tokenBalances":
|
|
accountDetails = api.AccountDetailsTokenBalances
|
|
case "txids":
|
|
accountDetails = api.AccountDetailsTxidHistory
|
|
case "txslight":
|
|
accountDetails = api.AccountDetailsTxHistoryLight
|
|
case "txs":
|
|
accountDetails = api.AccountDetailsTxHistory
|
|
}
|
|
tokensToReturn := api.TokensToReturnNonzeroBalance
|
|
switch r.URL.Query().Get("tokens") {
|
|
case "derived":
|
|
tokensToReturn = api.TokensToReturnDerived
|
|
case "used":
|
|
tokensToReturn = api.TokensToReturnUsed
|
|
case "nonzero":
|
|
tokensToReturn = api.TokensToReturnNonzeroBalance
|
|
}
|
|
gap, ec := strconv.Atoi(r.URL.Query().Get("gap"))
|
|
if ec != nil {
|
|
gap = 0
|
|
}
|
|
contract := r.URL.Query().Get("contract")
|
|
return page, pageSize, accountDetails, &api.AddressFilter{
|
|
Vout: voutFilter,
|
|
TokensToReturn: tokensToReturn,
|
|
FromHeight: uint32(from),
|
|
ToHeight: uint32(to),
|
|
Contract: contract,
|
|
}, filterParam, gap
|
|
}
|
|
|
|
func (s *PublicServer) explorerAddress(w http.ResponseWriter, r *http.Request) (tpl, *TemplateData, error) {
|
|
var addressParam string
|
|
i := strings.LastIndexByte(r.URL.Path, '/')
|
|
if i > 0 {
|
|
addressParam = r.URL.Path[i+1:]
|
|
}
|
|
if len(addressParam) == 0 {
|
|
return errorTpl, nil, api.NewAPIError("Missing address", true)
|
|
}
|
|
s.metrics.ExplorerViews.With(common.Labels{"action": "address"}).Inc()
|
|
page, _, _, filter, filterParam, _ := s.getAddressQueryParams(r, api.AccountDetailsTxHistoryLight, txsOnPage)
|
|
// do not allow details to be changed by query params
|
|
address, err := s.api.GetAddress(addressParam, page, txsOnPage, api.AccountDetailsTxHistoryLight, filter)
|
|
if err != nil {
|
|
return errorTpl, nil, err
|
|
}
|
|
data := s.newTemplateData()
|
|
data.AddrStr = address.AddrStr
|
|
data.Address = address
|
|
data.Page = address.Page
|
|
data.PagingRange, data.PrevPage, data.NextPage = getPagingRange(address.Page, address.TotalPages)
|
|
if filterParam == "" && filter.Vout > -1 {
|
|
filterParam = strconv.Itoa(filter.Vout)
|
|
}
|
|
if filterParam != "" {
|
|
data.PageParams = template.URL("&filter=" + filterParam)
|
|
data.Address.Filter = filterParam
|
|
}
|
|
return addressTpl, data, nil
|
|
}
|
|
|
|
func (s *PublicServer) explorerXpub(w http.ResponseWriter, r *http.Request) (tpl, *TemplateData, error) {
|
|
var xpub string
|
|
i := strings.LastIndexByte(r.URL.Path, '/')
|
|
if i > 0 {
|
|
xpub = r.URL.Path[i+1:]
|
|
}
|
|
if len(xpub) == 0 {
|
|
return errorTpl, nil, api.NewAPIError("Missing xpub", true)
|
|
}
|
|
s.metrics.ExplorerViews.With(common.Labels{"action": "xpub"}).Inc()
|
|
page, _, _, filter, filterParam, gap := s.getAddressQueryParams(r, api.AccountDetailsTxHistoryLight, txsOnPage)
|
|
// do not allow txsOnPage and details to be changed by query params
|
|
address, err := s.api.GetXpubAddress(xpub, page, txsOnPage, api.AccountDetailsTxHistoryLight, filter, gap)
|
|
if err != nil {
|
|
if err == api.ErrUnsupportedXpub {
|
|
err = api.NewAPIError("XPUB functionality is not supported", true)
|
|
}
|
|
return errorTpl, nil, err
|
|
}
|
|
data := s.newTemplateData()
|
|
data.AddrStr = address.AddrStr
|
|
data.Address = address
|
|
data.Page = address.Page
|
|
data.PagingRange, data.PrevPage, data.NextPage = getPagingRange(address.Page, address.TotalPages)
|
|
if filterParam != "" {
|
|
data.PageParams = template.URL("&filter=" + filterParam)
|
|
data.Address.Filter = filterParam
|
|
}
|
|
data.NonZeroBalanceTokens = filter.TokensToReturn == api.TokensToReturnNonzeroBalance
|
|
return xpubTpl, data, nil
|
|
}
|
|
|
|
func (s *PublicServer) explorerBlocks(w http.ResponseWriter, r *http.Request) (tpl, *TemplateData, error) {
|
|
var blocks *api.Blocks
|
|
var err error
|
|
s.metrics.ExplorerViews.With(common.Labels{"action": "blocks"}).Inc()
|
|
page, ec := strconv.Atoi(r.URL.Query().Get("page"))
|
|
if ec != nil {
|
|
page = 0
|
|
}
|
|
blocks, err = s.api.GetBlocks(page, blocksOnPage)
|
|
if err != nil {
|
|
return errorTpl, nil, err
|
|
}
|
|
data := s.newTemplateData()
|
|
data.Blocks = blocks
|
|
data.Page = blocks.Page
|
|
data.PagingRange, data.PrevPage, data.NextPage = getPagingRange(blocks.Page, blocks.TotalPages)
|
|
return blocksTpl, data, nil
|
|
}
|
|
|
|
func (s *PublicServer) explorerBlock(w http.ResponseWriter, r *http.Request) (tpl, *TemplateData, error) {
|
|
var block *api.Block
|
|
var err error
|
|
s.metrics.ExplorerViews.With(common.Labels{"action": "block"}).Inc()
|
|
if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 {
|
|
page, ec := strconv.Atoi(r.URL.Query().Get("page"))
|
|
if ec != nil {
|
|
page = 0
|
|
}
|
|
block, err = s.api.GetBlock(r.URL.Path[i+1:], page, txsOnPage)
|
|
if err != nil {
|
|
return errorTpl, nil, err
|
|
}
|
|
}
|
|
data := s.newTemplateData()
|
|
data.Block = block
|
|
data.Page = block.Page
|
|
data.PagingRange, data.PrevPage, data.NextPage = getPagingRange(block.Page, block.TotalPages)
|
|
return blockTpl, data, nil
|
|
}
|
|
|
|
func (s *PublicServer) explorerIndex(w http.ResponseWriter, r *http.Request) (tpl, *TemplateData, error) {
|
|
var si *api.SystemInfo
|
|
var err error
|
|
s.metrics.ExplorerViews.With(common.Labels{"action": "index"}).Inc()
|
|
si, err = s.api.GetSystemInfo(false)
|
|
if err != nil {
|
|
return errorTpl, nil, err
|
|
}
|
|
data := s.newTemplateData()
|
|
data.Info = si
|
|
return indexTpl, data, nil
|
|
}
|
|
|
|
func (s *PublicServer) explorerSearch(w http.ResponseWriter, r *http.Request) (tpl, *TemplateData, error) {
|
|
q := strings.TrimSpace(r.URL.Query().Get("q"))
|
|
var tx *api.Tx
|
|
var address *api.Address
|
|
var block *api.Block
|
|
var err error
|
|
s.metrics.ExplorerViews.With(common.Labels{"action": "search"}).Inc()
|
|
if len(q) > 0 {
|
|
address, err = s.api.GetXpubAddress(q, 0, 1, api.AccountDetailsBasic, &api.AddressFilter{Vout: api.AddressFilterVoutOff}, 0)
|
|
if err == nil {
|
|
http.Redirect(w, r, joinURL("/xpub/", address.AddrStr), 302)
|
|
return noTpl, nil, nil
|
|
}
|
|
block, err = s.api.GetBlock(q, 0, 1)
|
|
if err == nil {
|
|
http.Redirect(w, r, joinURL("/block/", block.Hash), 302)
|
|
return noTpl, nil, nil
|
|
}
|
|
tx, err = s.api.GetTransaction(q, false, false)
|
|
if err == nil {
|
|
http.Redirect(w, r, joinURL("/tx/", tx.Txid), 302)
|
|
return noTpl, nil, nil
|
|
}
|
|
address, err = s.api.GetAddress(q, 0, 1, api.AccountDetailsBasic, &api.AddressFilter{Vout: api.AddressFilterVoutOff})
|
|
if err == nil {
|
|
http.Redirect(w, r, joinURL("/address/", address.AddrStr), 302)
|
|
return noTpl, nil, nil
|
|
}
|
|
}
|
|
return errorTpl, nil, api.NewAPIError(fmt.Sprintf("No matching records found for '%v'", q), true)
|
|
}
|
|
|
|
func (s *PublicServer) explorerSendTx(w http.ResponseWriter, r *http.Request) (tpl, *TemplateData, error) {
|
|
s.metrics.ExplorerViews.With(common.Labels{"action": "sendtx"}).Inc()
|
|
data := s.newTemplateData()
|
|
if r.Method == http.MethodPost {
|
|
err := r.ParseForm()
|
|
if err != nil {
|
|
return sendTransactionTpl, data, err
|
|
}
|
|
hex := r.FormValue("hex")
|
|
if len(hex) > 0 {
|
|
res, err := s.chain.SendRawTransaction(hex)
|
|
if err != nil {
|
|
data.SendTxHex = hex
|
|
data.Error = &api.APIError{Text: err.Error(), Public: true}
|
|
return sendTransactionTpl, data, nil
|
|
}
|
|
data.Status = "Transaction sent, result " + res
|
|
}
|
|
}
|
|
return sendTransactionTpl, data, nil
|
|
}
|
|
|
|
func (s *PublicServer) explorerMempool(w http.ResponseWriter, r *http.Request) (tpl, *TemplateData, error) {
|
|
var mempoolTxids *api.MempoolTxids
|
|
var err error
|
|
s.metrics.ExplorerViews.With(common.Labels{"action": "mempool"}).Inc()
|
|
page, ec := strconv.Atoi(r.URL.Query().Get("page"))
|
|
if ec != nil {
|
|
page = 0
|
|
}
|
|
mempoolTxids, err = s.api.GetMempool(page, mempoolTxsOnPage)
|
|
if err != nil {
|
|
return errorTpl, nil, err
|
|
}
|
|
data := s.newTemplateData()
|
|
data.MempoolTxids = mempoolTxids
|
|
data.Page = mempoolTxids.Page
|
|
data.PagingRange, data.PrevPage, data.NextPage = getPagingRange(mempoolTxids.Page, mempoolTxids.TotalPages)
|
|
return mempoolTpl, data, nil
|
|
}
|
|
|
|
func getPagingRange(page int, total int) ([]int, int, int) {
|
|
// total==-1 means total is unknown, show only prev/next buttons
|
|
if total >= 0 && total < 2 {
|
|
return nil, 0, 0
|
|
}
|
|
var r []int
|
|
pp, np := page-1, page+1
|
|
if pp < 1 {
|
|
pp = 1
|
|
}
|
|
if total > 0 {
|
|
if np > total {
|
|
np = total
|
|
}
|
|
r = make([]int, 0, 8)
|
|
if total < 6 {
|
|
for i := 1; i <= total; i++ {
|
|
r = append(r, i)
|
|
}
|
|
} else {
|
|
r = append(r, 1)
|
|
if page > 3 {
|
|
r = append(r, 0)
|
|
}
|
|
if pp == 1 {
|
|
if page == 1 {
|
|
r = append(r, np)
|
|
r = append(r, np+1)
|
|
r = append(r, np+2)
|
|
} else {
|
|
r = append(r, page)
|
|
r = append(r, np)
|
|
r = append(r, np+1)
|
|
}
|
|
} else if np == total {
|
|
if page == total {
|
|
r = append(r, pp-2)
|
|
r = append(r, pp-1)
|
|
r = append(r, pp)
|
|
} else {
|
|
r = append(r, pp-1)
|
|
r = append(r, pp)
|
|
r = append(r, page)
|
|
}
|
|
} else {
|
|
r = append(r, pp)
|
|
r = append(r, page)
|
|
r = append(r, np)
|
|
}
|
|
if page <= total-3 {
|
|
r = append(r, 0)
|
|
}
|
|
r = append(r, total)
|
|
}
|
|
}
|
|
return r, pp, np
|
|
}
|
|
|
|
func (s *PublicServer) apiIndex(r *http.Request, apiVersion int) (interface{}, error) {
|
|
s.metrics.ExplorerViews.With(common.Labels{"action": "api-index"}).Inc()
|
|
return s.api.GetSystemInfo(false)
|
|
}
|
|
|
|
func (s *PublicServer) apiBlockIndex(r *http.Request, apiVersion int) (interface{}, error) {
|
|
type resBlockIndex struct {
|
|
BlockHash string `json:"blockHash"`
|
|
}
|
|
var err error
|
|
var hash string
|
|
height := -1
|
|
if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 {
|
|
if h, err := strconv.Atoi(r.URL.Path[i+1:]); err == nil {
|
|
height = h
|
|
}
|
|
}
|
|
if height >= 0 {
|
|
hash, err = s.db.GetBlockHash(uint32(height))
|
|
} else {
|
|
_, hash, err = s.db.GetBestBlock()
|
|
}
|
|
if err != nil {
|
|
glog.Error(err)
|
|
return nil, err
|
|
}
|
|
return resBlockIndex{
|
|
BlockHash: hash,
|
|
}, nil
|
|
}
|
|
|
|
func (s *PublicServer) apiTx(r *http.Request, apiVersion int) (interface{}, error) {
|
|
var txid string
|
|
i := strings.LastIndexByte(r.URL.Path, '/')
|
|
if i > 0 {
|
|
txid = r.URL.Path[i+1:]
|
|
}
|
|
if len(txid) == 0 {
|
|
return nil, api.NewAPIError("Missing txid", true)
|
|
}
|
|
var tx *api.Tx
|
|
var err error
|
|
s.metrics.ExplorerViews.With(common.Labels{"action": "api-tx"}).Inc()
|
|
spendingTxs := false
|
|
p := r.URL.Query().Get("spending")
|
|
if len(p) > 0 {
|
|
spendingTxs, err = strconv.ParseBool(p)
|
|
if err != nil {
|
|
return nil, api.NewAPIError("Parameter 'spending' cannot be converted to boolean", true)
|
|
}
|
|
}
|
|
tx, err = s.api.GetTransaction(txid, spendingTxs, false)
|
|
if err == nil && apiVersion == apiV1 {
|
|
return s.api.TxToV1(tx), nil
|
|
}
|
|
return tx, err
|
|
}
|
|
|
|
func (s *PublicServer) apiTxSpecific(r *http.Request, apiVersion int) (interface{}, error) {
|
|
var txid string
|
|
i := strings.LastIndexByte(r.URL.Path, '/')
|
|
if i > 0 {
|
|
txid = r.URL.Path[i+1:]
|
|
}
|
|
if len(txid) == 0 {
|
|
return nil, api.NewAPIError("Missing txid", true)
|
|
}
|
|
var tx json.RawMessage
|
|
var err error
|
|
s.metrics.ExplorerViews.With(common.Labels{"action": "api-tx-specific"}).Inc()
|
|
tx, err = s.chain.GetTransactionSpecific(&bchain.Tx{Txid: txid})
|
|
return tx, err
|
|
}
|
|
|
|
func (s *PublicServer) apiAddress(r *http.Request, apiVersion int) (interface{}, error) {
|
|
var addressParam string
|
|
i := strings.LastIndexByte(r.URL.Path, '/')
|
|
if i > 0 {
|
|
addressParam = r.URL.Path[i+1:]
|
|
}
|
|
if len(addressParam) == 0 {
|
|
return nil, api.NewAPIError("Missing address", true)
|
|
}
|
|
var address *api.Address
|
|
var err error
|
|
s.metrics.ExplorerViews.With(common.Labels{"action": "api-address"}).Inc()
|
|
page, pageSize, details, filter, _, _ := s.getAddressQueryParams(r, api.AccountDetailsTxidHistory, txsInAPI)
|
|
address, err = s.api.GetAddress(addressParam, page, pageSize, details, filter)
|
|
if err == nil && apiVersion == apiV1 {
|
|
return s.api.AddressToV1(address), nil
|
|
}
|
|
return address, err
|
|
}
|
|
|
|
func (s *PublicServer) apiXpub(r *http.Request, apiVersion int) (interface{}, error) {
|
|
var xpub string
|
|
i := strings.LastIndexByte(r.URL.Path, '/')
|
|
if i > 0 {
|
|
xpub = r.URL.Path[i+1:]
|
|
}
|
|
if len(xpub) == 0 {
|
|
return nil, api.NewAPIError("Missing xpub", true)
|
|
}
|
|
var address *api.Address
|
|
var err error
|
|
s.metrics.ExplorerViews.With(common.Labels{"action": "api-xpub"}).Inc()
|
|
page, pageSize, details, filter, _, gap := s.getAddressQueryParams(r, api.AccountDetailsTxidHistory, txsInAPI)
|
|
address, err = s.api.GetXpubAddress(xpub, page, pageSize, details, filter, gap)
|
|
if err == nil && apiVersion == apiV1 {
|
|
return s.api.AddressToV1(address), nil
|
|
}
|
|
if err == api.ErrUnsupportedXpub {
|
|
err = api.NewAPIError("XPUB functionality is not supported", true)
|
|
}
|
|
return address, err
|
|
}
|
|
|
|
func (s *PublicServer) apiUtxo(r *http.Request, apiVersion int) (interface{}, error) {
|
|
var utxo []api.Utxo
|
|
var err error
|
|
if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 {
|
|
onlyConfirmed := false
|
|
c := r.URL.Query().Get("confirmed")
|
|
if len(c) > 0 {
|
|
onlyConfirmed, err = strconv.ParseBool(c)
|
|
if err != nil {
|
|
return nil, api.NewAPIError("Parameter 'confirmed' cannot be converted to boolean", true)
|
|
}
|
|
}
|
|
gap, ec := strconv.Atoi(r.URL.Query().Get("gap"))
|
|
if ec != nil {
|
|
gap = 0
|
|
}
|
|
utxo, err = s.api.GetXpubUtxo(r.URL.Path[i+1:], onlyConfirmed, gap)
|
|
if err == nil {
|
|
s.metrics.ExplorerViews.With(common.Labels{"action": "api-xpub-utxo"}).Inc()
|
|
} else {
|
|
utxo, err = s.api.GetAddressUtxo(r.URL.Path[i+1:], onlyConfirmed)
|
|
s.metrics.ExplorerViews.With(common.Labels{"action": "api-address-utxo"}).Inc()
|
|
}
|
|
if err == nil && apiVersion == apiV1 {
|
|
return s.api.AddressUtxoToV1(utxo), nil
|
|
}
|
|
}
|
|
return utxo, err
|
|
}
|
|
|
|
func (s *PublicServer) apiBalanceHistory(r *http.Request, apiVersion int) (interface{}, error) {
|
|
var history []api.BalanceHistory
|
|
var fromTimestamp, toTimestamp int64
|
|
var err error
|
|
if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 {
|
|
gap, ec := strconv.Atoi(r.URL.Query().Get("gap"))
|
|
if ec != nil {
|
|
gap = 0
|
|
}
|
|
from := r.URL.Query().Get("from")
|
|
if from != "" {
|
|
fromTimestamp, err = strconv.ParseInt(from, 10, 64)
|
|
if err != nil {
|
|
return history, err
|
|
}
|
|
}
|
|
to := r.URL.Query().Get("to")
|
|
if to != "" {
|
|
toTimestamp, err = strconv.ParseInt(to, 10, 64)
|
|
if err != nil {
|
|
return history, err
|
|
}
|
|
}
|
|
var groupBy uint64
|
|
groupBy, err = strconv.ParseUint(r.URL.Query().Get("groupBy"), 10, 32)
|
|
if err != nil || groupBy == 0 {
|
|
groupBy = 3600
|
|
}
|
|
fiat := r.URL.Query().Get("fiatcurrency")
|
|
var fiatArray []string
|
|
if fiat != "" {
|
|
fiatArray = []string{fiat}
|
|
}
|
|
history, err = s.api.GetXpubBalanceHistory(r.URL.Path[i+1:], fromTimestamp, toTimestamp, fiatArray, gap, uint32(groupBy))
|
|
if err == nil {
|
|
s.metrics.ExplorerViews.With(common.Labels{"action": "api-xpub-balancehistory"}).Inc()
|
|
} else {
|
|
history, err = s.api.GetBalanceHistory(r.URL.Path[i+1:], fromTimestamp, toTimestamp, fiatArray, uint32(groupBy))
|
|
s.metrics.ExplorerViews.With(common.Labels{"action": "api-address-balancehistory"}).Inc()
|
|
}
|
|
}
|
|
return history, err
|
|
}
|
|
|
|
func (s *PublicServer) apiBlock(r *http.Request, apiVersion int) (interface{}, error) {
|
|
var block *api.Block
|
|
var err error
|
|
s.metrics.ExplorerViews.With(common.Labels{"action": "api-block"}).Inc()
|
|
if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 {
|
|
page, ec := strconv.Atoi(r.URL.Query().Get("page"))
|
|
if ec != nil {
|
|
page = 0
|
|
}
|
|
block, err = s.api.GetBlock(r.URL.Path[i+1:], page, txsInAPI)
|
|
if err == nil && apiVersion == apiV1 {
|
|
return s.api.BlockToV1(block), nil
|
|
}
|
|
}
|
|
return block, err
|
|
}
|
|
|
|
func (s *PublicServer) apiFeeStats(r *http.Request, apiVersion int) (interface{}, error) {
|
|
var feeStats *api.FeeStats
|
|
var err error
|
|
s.metrics.ExplorerViews.With(common.Labels{"action": "api-feestats"}).Inc()
|
|
if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 {
|
|
feeStats, err = s.api.GetFeeStats(r.URL.Path[i+1:])
|
|
}
|
|
return feeStats, err
|
|
}
|
|
|
|
type resultSendTransaction struct {
|
|
Result string `json:"result"`
|
|
}
|
|
|
|
func (s *PublicServer) apiSendTx(r *http.Request, apiVersion int) (interface{}, error) {
|
|
var err error
|
|
var res resultSendTransaction
|
|
var hex string
|
|
s.metrics.ExplorerViews.With(common.Labels{"action": "api-sendtx"}).Inc()
|
|
if r.Method == http.MethodPost {
|
|
data, err := ioutil.ReadAll(r.Body)
|
|
if err != nil {
|
|
return nil, api.NewAPIError("Missing tx blob", true)
|
|
}
|
|
hex = string(data)
|
|
} else {
|
|
if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 {
|
|
hex = r.URL.Path[i+1:]
|
|
}
|
|
}
|
|
if len(hex) > 0 {
|
|
res.Result, err = s.chain.SendRawTransaction(hex)
|
|
if err != nil {
|
|
return nil, api.NewAPIError(err.Error(), true)
|
|
}
|
|
return res, nil
|
|
}
|
|
return nil, api.NewAPIError("Missing tx blob", true)
|
|
}
|
|
|
|
// apiTickersList returns a list of available FiatRates currencies
|
|
func (s *PublicServer) apiTickersList(r *http.Request, apiVersion int) (interface{}, error) {
|
|
s.metrics.ExplorerViews.With(common.Labels{"action": "api-tickers-list"}).Inc()
|
|
timestampString := strings.ToLower(r.URL.Query().Get("timestamp"))
|
|
timestamp, err := strconv.ParseInt(timestampString, 10, 64)
|
|
if err != nil {
|
|
return nil, api.NewAPIError("Parameter \"timestamp\" is not a valid Unix timestamp.", true)
|
|
}
|
|
result, err := s.api.GetFiatRatesTickersList(timestamp)
|
|
return result, err
|
|
}
|
|
|
|
// apiTickers returns FiatRates ticker prices for the specified block or timestamp.
|
|
func (s *PublicServer) apiTickers(r *http.Request, apiVersion int) (interface{}, error) {
|
|
var result *db.ResultTickerAsString
|
|
var err error
|
|
|
|
currency := strings.ToLower(r.URL.Query().Get("currency"))
|
|
var currencies []string
|
|
if currency != "" {
|
|
currencies = []string{currency}
|
|
}
|
|
|
|
if block := r.URL.Query().Get("block"); block != "" {
|
|
// Get tickers for specified block height or block hash
|
|
s.metrics.ExplorerViews.With(common.Labels{"action": "api-tickers-block"}).Inc()
|
|
result, err = s.api.GetFiatRatesForBlockID(block, currencies)
|
|
} else if timestampString := r.URL.Query().Get("timestamp"); timestampString != "" {
|
|
// Get tickers for specified timestamp
|
|
s.metrics.ExplorerViews.With(common.Labels{"action": "api-tickers-date"}).Inc()
|
|
|
|
timestamp, err := strconv.ParseInt(timestampString, 10, 64)
|
|
if err != nil {
|
|
return nil, api.NewAPIError("Parameter \"timestamp\" is not a valid Unix timestamp.", true)
|
|
}
|
|
|
|
resultTickers, err := s.api.GetFiatRatesForTimestamps([]int64{timestamp}, currencies)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result = &resultTickers.Tickers[0]
|
|
} else {
|
|
// No parameters - get the latest available ticker
|
|
s.metrics.ExplorerViews.With(common.Labels{"action": "api-tickers-last"}).Inc()
|
|
result, err = s.api.GetCurrentFiatRates(currencies)
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
type resultEstimateFeeAsString struct {
|
|
Result string `json:"result"`
|
|
}
|
|
|
|
func (s *PublicServer) apiEstimateFee(r *http.Request, apiVersion int) (interface{}, error) {
|
|
var res resultEstimateFeeAsString
|
|
s.metrics.ExplorerViews.With(common.Labels{"action": "api-estimatefee"}).Inc()
|
|
if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 {
|
|
b := r.URL.Path[i+1:]
|
|
if len(b) > 0 {
|
|
blocks, err := strconv.Atoi(b)
|
|
if err != nil {
|
|
return nil, api.NewAPIError("Parameter 'number of blocks' is not a number", true)
|
|
}
|
|
conservative := true
|
|
c := r.URL.Query().Get("conservative")
|
|
if len(c) > 0 {
|
|
conservative, err = strconv.ParseBool(c)
|
|
if err != nil {
|
|
return nil, api.NewAPIError("Parameter 'conservative' cannot be converted to boolean", true)
|
|
}
|
|
}
|
|
var fee big.Int
|
|
fee, err = s.chain.EstimateSmartFee(blocks, conservative)
|
|
if err != nil {
|
|
fee, err = s.chain.EstimateFee(blocks)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
res.Result = s.chainParser.AmountToDecimalString(&fee)
|
|
return res, nil
|
|
}
|
|
}
|
|
return nil, api.NewAPIError("Missing parameter 'number of blocks'", true)
|
|
}
|