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