github.com/aychain/blockbook@v0.1.1-0.20181121092459-6d1fc7e07c5b/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 "reflect" 16 "runtime" 17 "strconv" 18 "strings" 19 "time" 20 21 "github.com/golang/glog" 22 ) 23 24 const txsOnPage = 25 25 const blocksOnPage = 50 26 const txsInAPI = 1000 27 28 // PublicServer is a handle to public http server 29 type PublicServer struct { 30 binding string 31 certFiles string 32 socketio *SocketIoServer 33 https *http.Server 34 db *db.RocksDB 35 txCache *db.TxCache 36 chain bchain.BlockChain 37 chainParser bchain.BlockChainParser 38 api *api.Worker 39 explorerURL string 40 internalExplorer bool 41 metrics *common.Metrics 42 is *common.InternalState 43 templates []*template.Template 44 debug bool 45 } 46 47 // NewPublicServer creates new public server http interface to blockbook and returns its handle 48 // only basic functionality is mapped, to map all functions, call 49 func NewPublicServer(binding string, certFiles string, db *db.RocksDB, chain bchain.BlockChain, txCache *db.TxCache, explorerURL string, metrics *common.Metrics, is *common.InternalState, debugMode bool) (*PublicServer, error) { 50 51 api, err := api.NewWorker(db, chain, txCache, is) 52 if err != nil { 53 return nil, err 54 } 55 56 socketio, err := NewSocketIoServer(db, chain, txCache, metrics, is) 57 if err != nil { 58 return nil, err 59 } 60 61 addr, path := splitBinding(binding) 62 serveMux := http.NewServeMux() 63 https := &http.Server{ 64 Addr: addr, 65 Handler: serveMux, 66 } 67 68 s := &PublicServer{ 69 binding: binding, 70 certFiles: certFiles, 71 https: https, 72 api: api, 73 socketio: socketio, 74 db: db, 75 txCache: txCache, 76 chain: chain, 77 chainParser: chain.GetChainParser(), 78 explorerURL: explorerURL, 79 internalExplorer: explorerURL == "", 80 metrics: metrics, 81 is: is, 82 debug: debugMode, 83 } 84 s.templates = parseTemplates() 85 86 // map only basic functions, the rest is enabled by method MapFullPublicInterface 87 serveMux.Handle(path+"favicon.ico", http.FileServer(http.Dir("./static/"))) 88 serveMux.Handle(path+"static/", http.StripPrefix("/static/", http.FileServer(http.Dir("./static/")))) 89 // default handler 90 serveMux.HandleFunc(path, s.htmlTemplateHandler(s.explorerIndex)) 91 // default API handler 92 serveMux.HandleFunc(path+"api/", s.jsonHandler(s.apiIndex)) 93 94 return s, nil 95 } 96 97 // Run starts the server 98 func (s *PublicServer) Run() error { 99 if s.certFiles == "" { 100 glog.Info("public server: starting to listen on http://", s.https.Addr) 101 return s.https.ListenAndServe() 102 } 103 glog.Info("public server starting to listen on https://", s.https.Addr) 104 return s.https.ListenAndServeTLS(fmt.Sprint(s.certFiles, ".crt"), fmt.Sprint(s.certFiles, ".key")) 105 } 106 107 // ConnectFullPublicInterface enables complete public functionality 108 func (s *PublicServer) ConnectFullPublicInterface() { 109 serveMux := s.https.Handler.(*http.ServeMux) 110 _, path := splitBinding(s.binding) 111 // support for tests of socket.io interface 112 serveMux.Handle(path+"test.html", http.FileServer(http.Dir("./static/"))) 113 if s.internalExplorer { 114 // internal explorer handlers 115 serveMux.HandleFunc(path+"tx/", s.htmlTemplateHandler(s.explorerTx)) 116 serveMux.HandleFunc(path+"address/", s.htmlTemplateHandler(s.explorerAddress)) 117 serveMux.HandleFunc(path+"search/", s.htmlTemplateHandler(s.explorerSearch)) 118 serveMux.HandleFunc(path+"blocks", s.htmlTemplateHandler(s.explorerBlocks)) 119 serveMux.HandleFunc(path+"block/", s.htmlTemplateHandler(s.explorerBlock)) 120 serveMux.HandleFunc(path+"spending/", s.htmlTemplateHandler(s.explorerSpendingTx)) 121 serveMux.HandleFunc(path+"sendtx", s.htmlTemplateHandler(s.explorerSendTx)) 122 } else { 123 // redirect to wallet requests for tx and address, possibly to external site 124 serveMux.HandleFunc(path+"tx/", s.txRedirect) 125 serveMux.HandleFunc(path+"address/", s.addressRedirect) 126 } 127 // API calls 128 serveMux.HandleFunc(path+"api/block-index/", s.jsonHandler(s.apiBlockIndex)) 129 serveMux.HandleFunc(path+"api/tx/", s.jsonHandler(s.apiTx)) 130 serveMux.HandleFunc(path+"api/tx-specific/", s.jsonHandler(s.apiTxSpecific)) 131 serveMux.HandleFunc(path+"api/address/", s.jsonHandler(s.apiAddress)) 132 serveMux.HandleFunc(path+"api/utxo/", s.jsonHandler(s.apiAddressUtxo)) 133 serveMux.HandleFunc(path+"api/block/", s.jsonHandler(s.apiBlock)) 134 serveMux.HandleFunc(path+"api/sendtx/", s.jsonHandler(s.apiSendTx)) 135 serveMux.HandleFunc(path+"api/estimatefee/", s.jsonHandler(s.apiEstimateFee)) 136 // socket.io interface 137 serveMux.Handle(path+"socket.io/", s.socketio.GetHandler()) 138 } 139 140 // Close closes the server 141 func (s *PublicServer) Close() error { 142 glog.Infof("public server: closing") 143 return s.https.Close() 144 } 145 146 // Shutdown shuts down the server 147 func (s *PublicServer) Shutdown(ctx context.Context) error { 148 glog.Infof("public server: shutdown") 149 return s.https.Shutdown(ctx) 150 } 151 152 // OnNewBlock notifies users subscribed to bitcoind/hashblock about new block 153 func (s *PublicServer) OnNewBlock(hash string, height uint32) { 154 s.socketio.OnNewBlockHash(hash) 155 } 156 157 // OnNewTxAddr notifies users subscribed to bitcoind/addresstxid about new block 158 func (s *PublicServer) OnNewTxAddr(txid string, desc bchain.AddressDescriptor, isOutput bool) { 159 s.socketio.OnNewTxAddr(txid, desc, isOutput) 160 } 161 162 func (s *PublicServer) txRedirect(w http.ResponseWriter, r *http.Request) { 163 http.Redirect(w, r, joinURL(s.explorerURL, r.URL.Path), 302) 164 s.metrics.ExplorerViews.With(common.Labels{"action": "tx-redirect"}).Inc() 165 } 166 167 func (s *PublicServer) addressRedirect(w http.ResponseWriter, r *http.Request) { 168 http.Redirect(w, r, joinURL(s.explorerURL, r.URL.Path), 302) 169 s.metrics.ExplorerViews.With(common.Labels{"action": "address-redirect"}).Inc() 170 } 171 172 func splitBinding(binding string) (addr string, path string) { 173 i := strings.Index(binding, "/") 174 if i >= 0 { 175 return binding[0:i], binding[i:] 176 } 177 return binding, "/" 178 } 179 180 func joinURL(base string, part string) string { 181 if len(base) > 0 { 182 if len(base) > 0 && base[len(base)-1] == '/' && len(part) > 0 && part[0] == '/' { 183 return base + part[1:] 184 } 185 return base + part 186 } 187 return part 188 } 189 190 func getFunctionName(i interface{}) string { 191 return runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name() 192 } 193 194 func (s *PublicServer) jsonHandler(handler func(r *http.Request) (interface{}, error)) func(w http.ResponseWriter, r *http.Request) { 195 type jsonError struct { 196 Text string `json:"error"` 197 HTTPStatus int `json:"-"` 198 } 199 return func(w http.ResponseWriter, r *http.Request) { 200 var data interface{} 201 var err error 202 defer func() { 203 if e := recover(); e != nil { 204 glog.Error(getFunctionName(handler), " recovered from panic: ", e) 205 if s.debug { 206 data = jsonError{fmt.Sprint("Internal server error: recovered from panic ", e), http.StatusInternalServerError} 207 } else { 208 data = jsonError{"Internal server error", http.StatusInternalServerError} 209 } 210 } 211 w.Header().Set("Content-Type", "application/json; charset=utf-8") 212 if e, isError := data.(jsonError); isError { 213 w.WriteHeader(e.HTTPStatus) 214 } 215 json.NewEncoder(w).Encode(data) 216 }() 217 data, err = handler(r) 218 if err != nil || data == nil { 219 if apiErr, ok := err.(*api.APIError); ok { 220 if apiErr.Public { 221 data = jsonError{apiErr.Error(), http.StatusBadRequest} 222 } else { 223 data = jsonError{apiErr.Error(), http.StatusInternalServerError} 224 } 225 } else { 226 if err != nil { 227 glog.Error(getFunctionName(handler), " error: ", err) 228 } 229 if s.debug { 230 if data != nil { 231 data = jsonError{fmt.Sprintf("Internal server error: %v, data %+v", err, data), http.StatusInternalServerError} 232 } else { 233 data = jsonError{fmt.Sprintf("Internal server error: %v", err), http.StatusInternalServerError} 234 } 235 } else { 236 data = jsonError{"Internal server error", http.StatusInternalServerError} 237 } 238 } 239 } 240 } 241 } 242 243 func (s *PublicServer) newTemplateData() *TemplateData { 244 return &TemplateData{ 245 CoinName: s.is.Coin, 246 CoinShortcut: s.is.CoinShortcut, 247 CoinLabel: s.is.CoinLabel, 248 InternalExplorer: s.internalExplorer && !s.is.InitialSync, 249 TOSLink: api.Text.TOSLink, 250 } 251 } 252 253 func (s *PublicServer) newTemplateDataWithError(text string) *TemplateData { 254 td := s.newTemplateData() 255 td.Error = &api.APIError{Text: text} 256 return td 257 } 258 259 func (s *PublicServer) htmlTemplateHandler(handler func(w http.ResponseWriter, r *http.Request) (tpl, *TemplateData, error)) func(w http.ResponseWriter, r *http.Request) { 260 return func(w http.ResponseWriter, r *http.Request) { 261 var t tpl 262 var data *TemplateData 263 var err error 264 defer func() { 265 if e := recover(); e != nil { 266 glog.Error(getFunctionName(handler), " recovered from panic: ", e) 267 t = errorInternalTpl 268 if s.debug { 269 data = s.newTemplateDataWithError(fmt.Sprint("Internal server error: recovered from panic ", e)) 270 } else { 271 data = s.newTemplateDataWithError("Internal server error") 272 } 273 } 274 // noTpl means the handler completely handled the request 275 if t != noTpl { 276 w.Header().Set("Content-Type", "text/html; charset=utf-8") 277 // return 500 Internal Server Error with errorInternalTpl 278 if t == errorInternalTpl { 279 w.WriteHeader(http.StatusInternalServerError) 280 } 281 if err := s.templates[t].ExecuteTemplate(w, "base.html", data); err != nil { 282 glog.Error(err) 283 } 284 } 285 }() 286 if s.debug { 287 // reload templates on each request 288 // to reflect changes during development 289 s.templates = parseTemplates() 290 } 291 t, data, err = handler(w, r) 292 if err != nil || (data == nil && t != noTpl) { 293 t = errorInternalTpl 294 if apiErr, ok := err.(*api.APIError); ok { 295 data = s.newTemplateData() 296 data.Error = apiErr 297 if apiErr.Public { 298 t = errorTpl 299 } 300 } else { 301 if err != nil { 302 glog.Error(getFunctionName(handler), " error: ", err) 303 } 304 if s.debug { 305 data = s.newTemplateDataWithError(fmt.Sprintf("Internal server error: %v, data %+v", err, data)) 306 } else { 307 data = s.newTemplateDataWithError("Internal server error") 308 } 309 } 310 } 311 } 312 } 313 314 type tpl int 315 316 const ( 317 noTpl = tpl(iota) 318 errorTpl 319 errorInternalTpl 320 indexTpl 321 txTpl 322 addressTpl 323 blocksTpl 324 blockTpl 325 sendTransactionTpl 326 327 tplCount 328 ) 329 330 // TemplateData is used to transfer data to the templates 331 type TemplateData struct { 332 CoinName string 333 CoinShortcut string 334 CoinLabel string 335 InternalExplorer bool 336 Address *api.Address 337 AddrStr string 338 Tx *api.Tx 339 TxSpecific json.RawMessage 340 Error *api.APIError 341 Blocks *api.Blocks 342 Block *api.Block 343 Info *api.SystemInfo 344 Page int 345 PrevPage int 346 NextPage int 347 PagingRange []int 348 TOSLink string 349 SendTxHex string 350 Status string 351 } 352 353 func parseTemplates() []*template.Template { 354 templateFuncMap := template.FuncMap{ 355 "formatTime": formatTime, 356 "formatUnixTime": formatUnixTime, 357 "formatAmount": formatAmount, 358 "setTxToTemplateData": setTxToTemplateData, 359 "stringInSlice": stringInSlice, 360 } 361 t := make([]*template.Template, tplCount) 362 t[errorTpl] = template.Must(template.New("error").Funcs(templateFuncMap).ParseFiles("./static/templates/error.html", "./static/templates/base.html")) 363 t[errorInternalTpl] = template.Must(template.New("error").Funcs(templateFuncMap).ParseFiles("./static/templates/error.html", "./static/templates/base.html")) 364 t[indexTpl] = template.Must(template.New("index").Funcs(templateFuncMap).ParseFiles("./static/templates/index.html", "./static/templates/base.html")) 365 t[txTpl] = template.Must(template.New("tx").Funcs(templateFuncMap).ParseFiles("./static/templates/tx.html", "./static/templates/txdetail.html", "./static/templates/base.html")) 366 t[addressTpl] = template.Must(template.New("address").Funcs(templateFuncMap).ParseFiles("./static/templates/address.html", "./static/templates/txdetail.html", "./static/templates/paging.html", "./static/templates/base.html")) 367 t[blocksTpl] = template.Must(template.New("blocks").Funcs(templateFuncMap).ParseFiles("./static/templates/blocks.html", "./static/templates/paging.html", "./static/templates/base.html")) 368 t[blockTpl] = template.Must(template.New("block").Funcs(templateFuncMap).ParseFiles("./static/templates/block.html", "./static/templates/txdetail.html", "./static/templates/paging.html", "./static/templates/base.html")) 369 t[sendTransactionTpl] = template.Must(template.New("block").Funcs(templateFuncMap).ParseFiles("./static/templates/sendtx.html", "./static/templates/base.html")) 370 return t 371 } 372 373 func formatUnixTime(ut int64) string { 374 return formatTime(time.Unix(ut, 0)) 375 } 376 377 func formatTime(t time.Time) string { 378 return t.Format(time.RFC1123) 379 } 380 381 // for now return the string as it is 382 // in future could be used to do coin specific formatting 383 func formatAmount(a string) string { 384 return a 385 } 386 387 // called from template to support txdetail.html functionality 388 func setTxToTemplateData(td *TemplateData, tx *api.Tx) *TemplateData { 389 td.Tx = tx 390 return td 391 } 392 393 func (s *PublicServer) explorerTx(w http.ResponseWriter, r *http.Request) (tpl, *TemplateData, error) { 394 var tx *api.Tx 395 var txSpecific json.RawMessage 396 var err error 397 s.metrics.ExplorerViews.With(common.Labels{"action": "tx"}).Inc() 398 if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 { 399 txid := r.URL.Path[i+1:] 400 tx, err = s.api.GetTransaction(txid, false) 401 if err != nil { 402 return errorTpl, nil, err 403 } 404 txSpecific, err = s.chain.GetTransactionSpecific(txid) 405 if err != nil { 406 return errorTpl, nil, err 407 } 408 } 409 data := s.newTemplateData() 410 data.Tx = tx 411 data.TxSpecific = txSpecific 412 return txTpl, data, nil 413 } 414 415 func (s *PublicServer) explorerSpendingTx(w http.ResponseWriter, r *http.Request) (tpl, *TemplateData, error) { 416 s.metrics.ExplorerViews.With(common.Labels{"action": "spendingtx"}).Inc() 417 var err error 418 parts := strings.Split(r.URL.Path, "/") 419 if len(parts) > 2 { 420 tx := parts[len(parts)-2] 421 n, ec := strconv.Atoi(parts[len(parts)-1]) 422 if ec == nil { 423 spendingTx, err := s.api.GetSpendingTxid(tx, n) 424 if err == nil && spendingTx != "" { 425 http.Redirect(w, r, joinURL("/tx/", spendingTx), 302) 426 return noTpl, nil, nil 427 } 428 } 429 } 430 if err == nil { 431 err = api.NewAPIError("Transaction not found", true) 432 } 433 return errorTpl, nil, err 434 } 435 436 func (s *PublicServer) explorerAddress(w http.ResponseWriter, r *http.Request) (tpl, *TemplateData, error) { 437 var address *api.Address 438 var err error 439 s.metrics.ExplorerViews.With(common.Labels{"action": "address"}).Inc() 440 if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 { 441 page, ec := strconv.Atoi(r.URL.Query().Get("page")) 442 if ec != nil { 443 page = 0 444 } 445 address, err = s.api.GetAddress(r.URL.Path[i+1:], page, txsOnPage, false) 446 if err != nil { 447 return errorTpl, nil, err 448 } 449 } 450 data := s.newTemplateData() 451 data.AddrStr = address.AddrStr 452 data.Address = address 453 data.Page = address.Page 454 data.PagingRange, data.PrevPage, data.NextPage = getPagingRange(address.Page, address.TotalPages) 455 return addressTpl, data, nil 456 } 457 458 func (s *PublicServer) explorerBlocks(w http.ResponseWriter, r *http.Request) (tpl, *TemplateData, error) { 459 var blocks *api.Blocks 460 var err error 461 s.metrics.ExplorerViews.With(common.Labels{"action": "blocks"}).Inc() 462 page, ec := strconv.Atoi(r.URL.Query().Get("page")) 463 if ec != nil { 464 page = 0 465 } 466 blocks, err = s.api.GetBlocks(page, blocksOnPage) 467 if err != nil { 468 return errorTpl, nil, err 469 } 470 data := s.newTemplateData() 471 data.Blocks = blocks 472 data.Page = blocks.Page 473 data.PagingRange, data.PrevPage, data.NextPage = getPagingRange(blocks.Page, blocks.TotalPages) 474 return blocksTpl, data, nil 475 } 476 477 func (s *PublicServer) explorerBlock(w http.ResponseWriter, r *http.Request) (tpl, *TemplateData, error) { 478 var block *api.Block 479 var err error 480 s.metrics.ExplorerViews.With(common.Labels{"action": "block"}).Inc() 481 if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 { 482 page, ec := strconv.Atoi(r.URL.Query().Get("page")) 483 if ec != nil { 484 page = 0 485 } 486 block, err = s.api.GetBlock(r.URL.Path[i+1:], page, txsOnPage) 487 if err != nil { 488 return errorTpl, nil, err 489 } 490 } 491 data := s.newTemplateData() 492 data.Block = block 493 data.Page = block.Page 494 data.PagingRange, data.PrevPage, data.NextPage = getPagingRange(block.Page, block.TotalPages) 495 return blockTpl, data, nil 496 } 497 498 func (s *PublicServer) explorerIndex(w http.ResponseWriter, r *http.Request) (tpl, *TemplateData, error) { 499 var si *api.SystemInfo 500 var err error 501 s.metrics.ExplorerViews.With(common.Labels{"action": "index"}).Inc() 502 si, err = s.api.GetSystemInfo(false) 503 if err != nil { 504 return errorTpl, nil, err 505 } 506 data := s.newTemplateData() 507 data.Info = si 508 return indexTpl, data, nil 509 } 510 511 func (s *PublicServer) explorerSearch(w http.ResponseWriter, r *http.Request) (tpl, *TemplateData, error) { 512 q := strings.TrimSpace(r.URL.Query().Get("q")) 513 var tx *api.Tx 514 var address *api.Address 515 var block *api.Block 516 var err error 517 s.metrics.ExplorerViews.With(common.Labels{"action": "search"}).Inc() 518 if len(q) > 0 { 519 block, err = s.api.GetBlock(q, 0, 1) 520 if err == nil { 521 http.Redirect(w, r, joinURL("/block/", block.Hash), 302) 522 return noTpl, nil, nil 523 } 524 tx, err = s.api.GetTransaction(q, false) 525 if err == nil { 526 http.Redirect(w, r, joinURL("/tx/", tx.Txid), 302) 527 return noTpl, nil, nil 528 } 529 address, err = s.api.GetAddress(q, 0, 1, true) 530 if err == nil { 531 http.Redirect(w, r, joinURL("/address/", address.AddrStr), 302) 532 return noTpl, nil, nil 533 } 534 } 535 return errorTpl, nil, api.NewAPIError(fmt.Sprintf("No matching records found for '%v'", q), true) 536 } 537 538 func (s *PublicServer) explorerSendTx(w http.ResponseWriter, r *http.Request) (tpl, *TemplateData, error) { 539 s.metrics.ExplorerViews.With(common.Labels{"action": "sendtx"}).Inc() 540 data := s.newTemplateData() 541 if r.Method == http.MethodPost { 542 err := r.ParseForm() 543 if err != nil { 544 return sendTransactionTpl, data, err 545 } 546 hex := r.FormValue("hex") 547 if len(hex) > 0 { 548 res, err := s.chain.SendRawTransaction(hex) 549 if err != nil { 550 data.SendTxHex = hex 551 data.Error = &api.APIError{Text: err.Error(), Public: true} 552 return sendTransactionTpl, data, nil 553 } 554 data.Status = "Transaction sent, result " + res 555 } 556 } 557 return sendTransactionTpl, data, nil 558 } 559 560 func getPagingRange(page int, total int) ([]int, int, int) { 561 if total < 2 { 562 return nil, 0, 0 563 } 564 pp, np := page-1, page+1 565 if np > total { 566 np = total 567 } 568 if pp < 1 { 569 pp = 1 570 } 571 r := make([]int, 0, 8) 572 if total < 6 { 573 for i := 1; i <= total; i++ { 574 r = append(r, i) 575 } 576 } else { 577 r = append(r, 1) 578 if page > 3 { 579 r = append(r, 0) 580 } 581 if pp == 1 { 582 if page == 1 { 583 r = append(r, np) 584 r = append(r, np+1) 585 r = append(r, np+2) 586 } else { 587 r = append(r, page) 588 r = append(r, np) 589 r = append(r, np+1) 590 } 591 } else if np == total { 592 if page == total { 593 r = append(r, pp-2) 594 r = append(r, pp-1) 595 r = append(r, pp) 596 } else { 597 r = append(r, pp-1) 598 r = append(r, pp) 599 r = append(r, page) 600 } 601 } else { 602 r = append(r, pp) 603 r = append(r, page) 604 r = append(r, np) 605 } 606 if page <= total-3 { 607 r = append(r, 0) 608 } 609 r = append(r, total) 610 } 611 return r, pp, np 612 } 613 614 func (s *PublicServer) apiIndex(r *http.Request) (interface{}, error) { 615 s.metrics.ExplorerViews.With(common.Labels{"action": "api-index"}).Inc() 616 return s.api.GetSystemInfo(false) 617 } 618 619 func (s *PublicServer) apiBlockIndex(r *http.Request) (interface{}, error) { 620 type resBlockIndex struct { 621 BlockHash string `json:"blockHash"` 622 } 623 var err error 624 var hash string 625 height := -1 626 if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 { 627 if h, err := strconv.Atoi(r.URL.Path[i+1:]); err == nil { 628 height = h 629 } 630 } 631 if height >= 0 { 632 hash, err = s.db.GetBlockHash(uint32(height)) 633 } else { 634 _, hash, err = s.db.GetBestBlock() 635 } 636 if err != nil { 637 glog.Error(err) 638 return nil, err 639 } 640 return resBlockIndex{ 641 BlockHash: hash, 642 }, nil 643 } 644 645 func (s *PublicServer) apiTx(r *http.Request) (interface{}, error) { 646 var tx *api.Tx 647 var err error 648 s.metrics.ExplorerViews.With(common.Labels{"action": "api-tx"}).Inc() 649 if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 { 650 txid := r.URL.Path[i+1:] 651 spendingTxs := false 652 p := r.URL.Query().Get("spending") 653 if len(p) > 0 { 654 spendingTxs, err = strconv.ParseBool(p) 655 if err != nil { 656 return nil, api.NewAPIError("Parameter 'spending' cannot be converted to boolean", true) 657 } 658 } 659 tx, err = s.api.GetTransaction(txid, spendingTxs) 660 } 661 return tx, err 662 } 663 664 func (s *PublicServer) apiTxSpecific(r *http.Request) (interface{}, error) { 665 var tx json.RawMessage 666 var err error 667 s.metrics.ExplorerViews.With(common.Labels{"action": "api-tx-specific"}).Inc() 668 if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 { 669 txid := r.URL.Path[i+1:] 670 tx, err = s.chain.GetTransactionSpecific(txid) 671 } 672 return tx, err 673 } 674 675 func (s *PublicServer) apiAddress(r *http.Request) (interface{}, error) { 676 var address *api.Address 677 var err error 678 s.metrics.ExplorerViews.With(common.Labels{"action": "api-address"}).Inc() 679 if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 { 680 page, ec := strconv.Atoi(r.URL.Query().Get("page")) 681 if ec != nil { 682 page = 0 683 } 684 address, err = s.api.GetAddress(r.URL.Path[i+1:], page, txsInAPI, true) 685 } 686 return address, err 687 } 688 689 func (s *PublicServer) apiAddressUtxo(r *http.Request) (interface{}, error) { 690 var utxo []api.AddressUtxo 691 var err error 692 s.metrics.ExplorerViews.With(common.Labels{"action": "api-address"}).Inc() 693 if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 { 694 onlyConfirmed := false 695 c := r.URL.Query().Get("confirmed") 696 if len(c) > 0 { 697 onlyConfirmed, err = strconv.ParseBool(c) 698 if err != nil { 699 return nil, api.NewAPIError("Parameter 'confirmed' cannot be converted to boolean", true) 700 } 701 } 702 utxo, err = s.api.GetAddressUtxo(r.URL.Path[i+1:], onlyConfirmed) 703 } 704 return utxo, err 705 } 706 707 func (s *PublicServer) apiBlock(r *http.Request) (interface{}, error) { 708 var block *api.Block 709 var err error 710 s.metrics.ExplorerViews.With(common.Labels{"action": "api-block"}).Inc() 711 if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 { 712 page, ec := strconv.Atoi(r.URL.Query().Get("page")) 713 if ec != nil { 714 page = 0 715 } 716 block, err = s.api.GetBlock(r.URL.Path[i+1:], page, txsInAPI) 717 } 718 return block, err 719 } 720 721 type resultSendTransaction struct { 722 Result string `json:"result"` 723 } 724 725 func (s *PublicServer) apiSendTx(r *http.Request) (interface{}, error) { 726 var err error 727 var res resultSendTransaction 728 var hex string 729 s.metrics.ExplorerViews.With(common.Labels{"action": "api-sendtx"}).Inc() 730 if r.Method == http.MethodPost { 731 data, err := ioutil.ReadAll(r.Body) 732 if err != nil { 733 return nil, api.NewAPIError("Missing tx blob", true) 734 } 735 hex = string(data) 736 } else { 737 if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 { 738 hex = r.URL.Path[i+1:] 739 } 740 } 741 if len(hex) > 0 { 742 res.Result, err = s.chain.SendRawTransaction(hex) 743 if err != nil { 744 return nil, api.NewAPIError(err.Error(), true) 745 } 746 return res, nil 747 } 748 return nil, api.NewAPIError("Missing tx blob", true) 749 } 750 751 type resultEstimateFeeAsString struct { 752 Result string `json:"result"` 753 } 754 755 func (s *PublicServer) apiEstimateFee(r *http.Request) (interface{}, error) { 756 var res resultEstimateFeeAsString 757 s.metrics.ExplorerViews.With(common.Labels{"action": "api-estimatefee"}).Inc() 758 if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 { 759 b := r.URL.Path[i+1:] 760 if len(b) > 0 { 761 blocks, err := strconv.Atoi(b) 762 if err != nil { 763 return nil, api.NewAPIError("Parameter 'number of blocks' is not a number", true) 764 } 765 conservative := true 766 c := r.URL.Query().Get("conservative") 767 if len(c) > 0 { 768 conservative, err = strconv.ParseBool(c) 769 if err != nil { 770 return nil, api.NewAPIError("Parameter 'conservative' cannot be converted to boolean", true) 771 } 772 } 773 var fee big.Int 774 fee, err = s.chain.EstimateSmartFee(blocks, conservative) 775 if err != nil { 776 fee, err = s.chain.EstimateFee(blocks) 777 if err != nil { 778 return nil, err 779 } 780 } 781 res.Result = s.chainParser.AmountToDecimalString(&fee) 782 return res, nil 783 } 784 } 785 return nil, api.NewAPIError("Missing parameter 'number of blocks'", true) 786 }