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