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