github.com/cryptohub-digital/blockbook-fork@v0.0.0-20230713133354-673c927af7f1/server/public.go (about) 1 package server 2 3 import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "html/template" 8 "io" 9 "math/big" 10 "net/http" 11 "net/url" 12 "os" 13 "path/filepath" 14 "reflect" 15 "regexp" 16 "runtime" 17 "runtime/debug" 18 "sort" 19 "strconv" 20 "strings" 21 "time" 22 23 "github.com/cryptohub-digital/blockbook-fork/api" 24 "github.com/cryptohub-digital/blockbook-fork/bchain" 25 "github.com/cryptohub-digital/blockbook-fork/common" 26 "github.com/cryptohub-digital/blockbook-fork/db" 27 "github.com/cryptohub-digital/blockbook-fork/fiat" 28 "github.com/golang/glog" 29 ) 30 31 const txsOnPage = 25 32 const blocksOnPage = 50 33 const mempoolTxsOnPage = 50 34 const txsInAPI = 1000 35 36 const secondaryCoinCookieName = "secondary_coin" 37 38 const ( 39 _ = iota 40 apiV1 41 apiV2 42 ) 43 44 // PublicServer provides public http server functionality 45 type PublicServer struct { 46 htmlTemplates[TemplateData] 47 binding string 48 certFiles string 49 socketio *SocketIoServer 50 websocket *WebsocketServer 51 https *http.Server 52 db *db.RocksDB 53 txCache *db.TxCache 54 chain bchain.BlockChain 55 chainParser bchain.BlockChainParser 56 mempool bchain.Mempool 57 api *api.Worker 58 explorerURL string 59 internalExplorer bool 60 is *common.InternalState 61 fiatRates *fiat.FiatRates 62 useSatsAmountFormat bool 63 } 64 65 // NewPublicServer creates new public server http interface to blockbook and returns its handle 66 // only basic functionality is mapped, to map all functions, call 67 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, fiatRates *fiat.FiatRates, debugMode bool) (*PublicServer, error) { 68 69 api, err := api.NewWorker(db, chain, mempool, txCache, metrics, is, fiatRates) 70 if err != nil { 71 return nil, err 72 } 73 74 socketio, err := NewSocketIoServer(db, chain, mempool, txCache, metrics, is, fiatRates) 75 if err != nil { 76 return nil, err 77 } 78 79 websocket, err := NewWebsocketServer(db, chain, mempool, txCache, metrics, is, fiatRates) 80 if err != nil { 81 return nil, err 82 } 83 84 addr, path := splitBinding(binding) 85 serveMux := http.NewServeMux() 86 https := &http.Server{ 87 Addr: addr, 88 Handler: serveMux, 89 } 90 91 s := &PublicServer{ 92 htmlTemplates: htmlTemplates[TemplateData]{ 93 metrics: metrics, 94 debug: debugMode, 95 }, 96 binding: binding, 97 certFiles: certFiles, 98 https: https, 99 api: api, 100 socketio: socketio, 101 websocket: websocket, 102 db: db, 103 txCache: txCache, 104 chain: chain, 105 chainParser: chain.GetChainParser(), 106 mempool: mempool, 107 explorerURL: explorerURL, 108 internalExplorer: explorerURL == "", 109 is: is, 110 fiatRates: fiatRates, 111 useSatsAmountFormat: chain.GetChainParser().GetChainType() == bchain.ChainBitcoinType && chain.GetChainParser().AmountDecimals() == 8, 112 } 113 s.htmlTemplates.newTemplateData = s.newTemplateData 114 s.htmlTemplates.newTemplateDataWithError = s.newTemplateDataWithError 115 s.htmlTemplates.parseTemplates = s.parseTemplates 116 s.htmlTemplates.postHtmlTemplateHandler = s.postHtmlTemplateHandler 117 s.templates = s.parseTemplates() 118 119 // map only basic functions, the rest is enabled by method MapFullPublicInterface 120 serveMux.Handle(path+"favicon.ico", http.FileServer(http.Dir("./static/"))) 121 serveMux.Handle(path+"static/", http.StripPrefix("/static/", http.FileServer(http.Dir("./static/")))) 122 // default handler 123 serveMux.HandleFunc(path, s.htmlTemplateHandler(s.explorerIndex)) 124 // default API handler 125 serveMux.HandleFunc(path+"api/", s.jsonHandler(s.apiIndex, apiV2)) 126 127 return s, nil 128 } 129 130 // Run starts the server 131 func (s *PublicServer) Run() error { 132 if s.certFiles == "" { 133 glog.Info("public server: starting to listen on http://", s.https.Addr) 134 return s.https.ListenAndServe() 135 } 136 glog.Info("public server starting to listen on https://", s.https.Addr) 137 return s.https.ListenAndServeTLS(fmt.Sprint(s.certFiles, ".crt"), fmt.Sprint(s.certFiles, ".key")) 138 } 139 140 // ConnectFullPublicInterface enables complete public functionality 141 func (s *PublicServer) ConnectFullPublicInterface() { 142 serveMux := s.https.Handler.(*http.ServeMux) 143 _, path := splitBinding(s.binding) 144 // support for test pages 145 serveMux.Handle(path+"test-socketio.html", http.FileServer(http.Dir("./static/"))) 146 serveMux.Handle(path+"test-websocket.html", http.FileServer(http.Dir("./static/"))) 147 if s.internalExplorer { 148 // internal explorer handlers 149 serveMux.HandleFunc(path+"tx/", s.htmlTemplateHandler(s.explorerTx)) 150 serveMux.HandleFunc(path+"address/", s.htmlTemplateHandler(s.explorerAddress)) 151 serveMux.HandleFunc(path+"xpub/", s.htmlTemplateHandler(s.explorerXpub)) 152 serveMux.HandleFunc(path+"search/", s.htmlTemplateHandler(s.explorerSearch)) 153 serveMux.HandleFunc(path+"blocks", s.htmlTemplateHandler(s.explorerBlocks)) 154 serveMux.HandleFunc(path+"block/", s.htmlTemplateHandler(s.explorerBlock)) 155 serveMux.HandleFunc(path+"spending/", s.htmlTemplateHandler(s.explorerSpendingTx)) 156 serveMux.HandleFunc(path+"sendtx", s.htmlTemplateHandler(s.explorerSendTx)) 157 serveMux.HandleFunc(path+"mempool", s.htmlTemplateHandler(s.explorerMempool)) 158 if s.chainParser.GetChainType() == bchain.ChainEthereumType { 159 serveMux.HandleFunc(path+"nft/", s.htmlTemplateHandler(s.explorerNftDetail)) 160 } 161 } else { 162 // redirect to wallet requests for tx and address, possibly to external site 163 serveMux.HandleFunc(path+"tx/", s.txRedirect) 164 serveMux.HandleFunc(path+"address/", s.addressRedirect) 165 } 166 // API calls 167 // default api without version can be changed to different version at any time 168 // use versioned api for stability 169 170 var apiDefault int 171 // ethereum supports only api V2 172 if s.chainParser.GetChainType() == bchain.ChainEthereumType || s.chainParser.GetChainType() == bchain.ChainCoreCoinType { 173 apiDefault = apiV2 174 } else { 175 apiDefault = apiV1 176 // legacy v1 format 177 serveMux.HandleFunc(path+"api/v1/block-index/", s.jsonHandler(s.apiBlockIndex, apiV1)) 178 serveMux.HandleFunc(path+"api/v1/tx-specific/", s.jsonHandler(s.apiTxSpecific, apiV1)) 179 serveMux.HandleFunc(path+"api/v1/tx/", s.jsonHandler(s.apiTx, apiV1)) 180 serveMux.HandleFunc(path+"api/v1/address/", s.jsonHandler(s.apiAddress, apiV1)) 181 serveMux.HandleFunc(path+"api/v1/utxo/", s.jsonHandler(s.apiUtxo, apiV1)) 182 serveMux.HandleFunc(path+"api/v1/block/", s.jsonHandler(s.apiBlock, apiV1)) 183 serveMux.HandleFunc(path+"api/v1/sendtx/", s.jsonHandler(s.apiSendTx, apiV1)) 184 serveMux.HandleFunc(path+"api/v1/estimatefee/", s.jsonHandler(s.apiEstimateFee, apiV1)) 185 } 186 serveMux.HandleFunc(path+"api/block-index/", s.jsonHandler(s.apiBlockIndex, apiDefault)) 187 serveMux.HandleFunc(path+"api/tx-specific/", s.jsonHandler(s.apiTxSpecific, apiDefault)) 188 serveMux.HandleFunc(path+"api/tx/", s.jsonHandler(s.apiTx, apiDefault)) 189 serveMux.HandleFunc(path+"api/address/", s.jsonHandler(s.apiAddress, apiDefault)) 190 serveMux.HandleFunc(path+"api/xpub/", s.jsonHandler(s.apiXpub, apiDefault)) 191 serveMux.HandleFunc(path+"api/utxo/", s.jsonHandler(s.apiUtxo, apiDefault)) 192 serveMux.HandleFunc(path+"api/block/", s.jsonHandler(s.apiBlock, apiDefault)) 193 serveMux.HandleFunc(path+"api/rawblock/", s.jsonHandler(s.apiBlockRaw, apiDefault)) 194 serveMux.HandleFunc(path+"api/sendtx/", s.jsonHandler(s.apiSendTx, apiDefault)) 195 serveMux.HandleFunc(path+"api/estimatefee/", s.jsonHandler(s.apiEstimateFee, apiDefault)) 196 serveMux.HandleFunc(path+"api/balancehistory/", s.jsonHandler(s.apiBalanceHistory, apiDefault)) 197 // v2 format 198 serveMux.HandleFunc(path+"api/v2/block-index/", s.jsonHandler(s.apiBlockIndex, apiV2)) 199 serveMux.HandleFunc(path+"api/v2/tx-specific/", s.jsonHandler(s.apiTxSpecific, apiV2)) 200 serveMux.HandleFunc(path+"api/v2/tx/", s.jsonHandler(s.apiTx, apiV2)) 201 serveMux.HandleFunc(path+"api/v2/address/", s.jsonHandler(s.apiAddress, apiV2)) 202 serveMux.HandleFunc(path+"api/v2/xpub/", s.jsonHandler(s.apiXpub, apiV2)) 203 serveMux.HandleFunc(path+"api/v2/utxo/", s.jsonHandler(s.apiUtxo, apiV2)) 204 serveMux.HandleFunc(path+"api/v2/block/", s.jsonHandler(s.apiBlock, apiV2)) 205 serveMux.HandleFunc(path+"api/v2/rawblock/", s.jsonHandler(s.apiBlockRaw, apiDefault)) 206 serveMux.HandleFunc(path+"api/v2/sendtx/", s.jsonHandler(s.apiSendTx, apiV2)) 207 serveMux.HandleFunc(path+"api/v2/estimatefee/", s.jsonHandler(s.apiEstimateFee, apiV2)) 208 serveMux.HandleFunc(path+"api/v2/feestats/", s.jsonHandler(s.apiFeeStats, apiV2)) 209 serveMux.HandleFunc(path+"api/v2/balancehistory/", s.jsonHandler(s.apiBalanceHistory, apiDefault)) 210 serveMux.HandleFunc(path+"api/v2/tickers/", s.jsonHandler(s.apiTickers, apiV2)) 211 serveMux.HandleFunc(path+"api/v2/multi-tickers/", s.jsonHandler(s.apiMultiTickers, apiV2)) 212 serveMux.HandleFunc(path+"api/v2/tickers-list/", s.jsonHandler(s.apiAvailableVsCurrencies, apiV2)) 213 // socket.io interface 214 serveMux.Handle(path+"socket.io/", s.socketio.GetHandler()) 215 // websocket interface 216 serveMux.Handle(path+"websocket", s.websocket.GetHandler()) 217 } 218 219 // Close closes the server 220 func (s *PublicServer) Close() error { 221 glog.Infof("public server: closing") 222 return s.https.Close() 223 } 224 225 // Shutdown shuts down the server 226 func (s *PublicServer) Shutdown(ctx context.Context) error { 227 glog.Infof("public server: shutdown") 228 return s.https.Shutdown(ctx) 229 } 230 231 // OnNewBlock notifies users subscribed to bitcoind/hashblock about new block 232 func (s *PublicServer) OnNewBlock(hash string, height uint32) { 233 s.socketio.OnNewBlockHash(hash) 234 s.websocket.OnNewBlock(hash, height) 235 } 236 237 // OnNewFiatRatesTicker notifies users subscribed to bitcoind/fiatrates about new ticker 238 func (s *PublicServer) OnNewFiatRatesTicker(ticker *common.CurrencyRatesTicker) { 239 s.websocket.OnNewFiatRatesTicker(ticker) 240 } 241 242 // OnNewTxAddr notifies users subscribed to notification about new tx 243 func (s *PublicServer) OnNewTxAddr(tx *bchain.Tx, desc bchain.AddressDescriptor) { 244 s.socketio.OnNewTxAddr(tx.Txid, desc) 245 } 246 247 // OnNewTx notifies users subscribed to notification about new tx 248 func (s *PublicServer) OnNewTx(tx *bchain.MempoolTx) { 249 s.websocket.OnNewTx(tx) 250 } 251 252 func (s *PublicServer) txRedirect(w http.ResponseWriter, r *http.Request) { 253 http.Redirect(w, r, joinURL(s.explorerURL, r.URL.Path), http.StatusFound) 254 s.metrics.ExplorerViews.With(common.Labels{"action": "tx-redirect"}).Inc() 255 } 256 257 func (s *PublicServer) addressRedirect(w http.ResponseWriter, r *http.Request) { 258 http.Redirect(w, r, joinURL(s.explorerURL, r.URL.Path), http.StatusFound) 259 s.metrics.ExplorerViews.With(common.Labels{"action": "address-redirect"}).Inc() 260 } 261 262 func splitBinding(binding string) (addr string, path string) { 263 i := strings.Index(binding, "/") 264 if i >= 0 { 265 return binding[0:i], binding[i:] 266 } 267 return binding, "/" 268 } 269 270 func joinURL(base string, part string) string { 271 if len(base) > 0 { 272 if len(base) > 0 && base[len(base)-1] == '/' && len(part) > 0 && part[0] == '/' { 273 return base + part[1:] 274 } 275 return base + part 276 } 277 return part 278 } 279 280 func getFunctionName(i interface{}) string { 281 name := runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name() 282 start := strings.LastIndex(name, ".") 283 end := strings.LastIndex(name, "-") 284 if start > 0 && end > start { 285 name = name[start+1 : end] 286 } 287 return name 288 } 289 290 func (s *PublicServer) jsonHandler(handler func(r *http.Request, apiVersion int) (interface{}, error), apiVersion int) func(w http.ResponseWriter, r *http.Request) { 291 type jsonError struct { 292 Text string `json:"error"` 293 HTTPStatus int `json:"-"` 294 } 295 handlerName := getFunctionName(handler) 296 return func(w http.ResponseWriter, r *http.Request) { 297 var data interface{} 298 var err error 299 defer func() { 300 if e := recover(); e != nil { 301 glog.Error(handlerName, " recovered from panic: ", e) 302 debug.PrintStack() 303 if s.debug { 304 data = jsonError{fmt.Sprint("Internal server error: recovered from panic ", e), http.StatusInternalServerError} 305 } else { 306 data = jsonError{"Internal server error", http.StatusInternalServerError} 307 } 308 } 309 w.Header().Set("Content-Type", "application/json; charset=utf-8") 310 if e, isError := data.(jsonError); isError { 311 w.WriteHeader(e.HTTPStatus) 312 } 313 err = json.NewEncoder(w).Encode(data) 314 if err != nil { 315 glog.Warning("json encode ", err) 316 } 317 s.metrics.ExplorerPendingRequests.With((common.Labels{"method": handlerName})).Dec() 318 }() 319 s.metrics.ExplorerPendingRequests.With((common.Labels{"method": handlerName})).Inc() 320 data, err = handler(r, apiVersion) 321 if err != nil || data == nil { 322 if apiErr, ok := err.(*api.APIError); ok { 323 if apiErr.Public { 324 data = jsonError{apiErr.Error(), http.StatusBadRequest} 325 } else { 326 data = jsonError{apiErr.Error(), http.StatusInternalServerError} 327 } 328 } else { 329 if err != nil { 330 glog.Error(handlerName, " error: ", err) 331 } 332 if s.debug { 333 if data != nil { 334 data = jsonError{fmt.Sprintf("Internal server error: %v, data %+v", err, data), http.StatusInternalServerError} 335 } else { 336 data = jsonError{fmt.Sprintf("Internal server error: %v", err), http.StatusInternalServerError} 337 } 338 } else { 339 data = jsonError{"Internal server error", http.StatusInternalServerError} 340 } 341 } 342 } 343 } 344 } 345 346 func (s *PublicServer) newTemplateData(r *http.Request) *TemplateData { 347 t := &TemplateData{ 348 CoinName: s.is.Coin, 349 CoinShortcut: s.is.CoinShortcut, 350 CoinLabel: s.is.CoinLabel, 351 ChainType: s.chainParser.GetChainType(), 352 InternalExplorer: s.internalExplorer && !s.is.InitialSync, 353 TOSLink: api.Text.TOSLink, 354 } 355 if t.ChainType == bchain.ChainEthereumType || t.ChainType == bchain.ChainCoreCoinType { 356 t.FungibleTokenName = bchain.TokenTypeMap[bchain.FungibleToken] 357 t.NonFungibleTokenName = bchain.TokenTypeMap[bchain.NonFungibleToken] 358 t.MultiTokenName = bchain.TokenTypeMap[bchain.MultiToken] 359 } 360 if !s.debug { 361 t.Minified = ".min.3" 362 } 363 if s.is.HasFiatRates { 364 // get the secondary coin and if it should be shown either from query parameters "secondary" and "use_secondary" 365 // or from the cookie "secondary_coin" in the format secondary=use_secondary, for example EUR=true 366 // the query parameters take precedence over the cookie 367 var cookieSecondary string 368 var cookieUseSecondary bool 369 cookie, _ := r.Cookie(secondaryCoinCookieName) 370 if cookie != nil { 371 a := strings.Split(cookie.Value, "=") 372 if len(a) == 2 { 373 cookieSecondary = a[0] 374 cookieUseSecondary, _ = strconv.ParseBool(a[1]) 375 } 376 } 377 secondary := strings.ToLower(r.URL.Query().Get("secondary")) 378 if secondary == "" { 379 if cookieSecondary != "" { 380 secondary = strings.ToLower(cookieSecondary) 381 } else { 382 secondary = "usd" 383 } 384 } 385 ticker := s.fiatRates.GetCurrentTicker(secondary, "") 386 if ticker == nil && secondary != "usd" { 387 secondary = "usd" 388 ticker = s.fiatRates.GetCurrentTicker(secondary, "") 389 } 390 if ticker != nil { 391 t.SecondaryCoin = strings.ToUpper(secondary) 392 t.CurrentSecondaryCoinRate = float64(ticker.Rates[secondary]) 393 t.CurrentTicker = ticker 394 t.SecondaryCurrencies = make([]string, 0, len(ticker.Rates)) 395 for k := range ticker.Rates { 396 t.SecondaryCurrencies = append(t.SecondaryCurrencies, strings.ToUpper(k)) 397 } 398 sort.Strings(t.SecondaryCurrencies) // sort to get deterministic results 399 t.UseSecondaryCoin, _ = strconv.ParseBool(r.URL.Query().Get("use_secondary")) 400 if !t.UseSecondaryCoin { 401 t.UseSecondaryCoin = cookieUseSecondary 402 } 403 } 404 } 405 return t 406 } 407 408 func (s *PublicServer) newTemplateDataWithError(error *api.APIError, r *http.Request) *TemplateData { 409 td := s.newTemplateData(r) 410 td.Error = error 411 return td 412 } 413 414 const ( 415 indexTpl = iota + errorInternalTpl + 1 416 txTpl 417 addressTpl 418 xpubTpl 419 blocksTpl 420 blockTpl 421 sendTransactionTpl 422 mempoolTpl 423 nftDetailTpl 424 425 publicTplCount 426 ) 427 428 // TemplateData is used to transfer data to the templates 429 type TemplateData struct { 430 CoinName string 431 CoinShortcut string 432 CoinLabel string 433 InternalExplorer bool 434 ChainType bchain.ChainType 435 FungibleTokenName bchain.TokenTypeName 436 NonFungibleTokenName bchain.TokenTypeName 437 MultiTokenName bchain.TokenTypeName 438 Address *api.Address 439 AddrStr string 440 Tx *api.Tx 441 Error *api.APIError 442 Blocks *api.Blocks 443 Block *api.Block 444 Info *api.SystemInfo 445 MempoolTxids *api.MempoolTxids 446 Page int 447 PrevPage int 448 NextPage int 449 PagingRange []int 450 PageParams template.URL 451 Minified string 452 TOSLink string 453 SendTxHex string 454 Status string 455 NonZeroBalanceTokens bool 456 TokenId string 457 URI string 458 ContractInfo *bchain.ContractInfo 459 SecondaryCoin string 460 UseSecondaryCoin bool 461 CurrentSecondaryCoinRate float64 462 CurrentTicker *common.CurrencyRatesTicker 463 SecondaryCurrencies []string 464 TxDate string 465 TxSecondaryCoinRate float64 466 TxTicker *common.CurrencyRatesTicker 467 } 468 469 func (s *PublicServer) parseTemplates() []*template.Template { 470 templateFuncMap := template.FuncMap{ 471 "timeSpan": timeSpan, 472 "relativeTime": relativeTime, 473 "unixTimeSpan": unixTimeSpan, 474 "amountSpan": s.amountSpan, 475 "tokenAmountSpan": s.tokenAmountSpan, 476 "amountSatsSpan": s.amountSatsSpan, 477 "formattedAmountSpan": s.formattedAmountSpan, 478 "summaryValuesSpan": s.summaryValuesSpan, 479 "addressAlias": addressAlias, 480 "addressAliasSpan": addressAliasSpan, 481 "formatAmount": s.formatAmount, 482 "formatAmountWithDecimals": formatAmountWithDecimals, 483 "formatInt64": formatInt64, 484 "formatInt": formatInt, 485 "formatUint32": formatUint32, 486 "formatBigInt": formatBigInt, 487 "setTxToTemplateData": setTxToTemplateData, 488 "feePerByte": feePerByte, 489 "isOwnAddress": isOwnAddress, 490 "toJSON": toJSON, 491 "tokenTransfersCount": tokenTransfersCount, 492 "tokenCount": tokenCount, 493 "hasPrefix": strings.HasPrefix, 494 "jsStr": jsStr, 495 } 496 var createTemplate func(filenames ...string) *template.Template 497 if s.debug { 498 createTemplate = func(filenames ...string) *template.Template { 499 if len(filenames) == 0 { 500 panic("Missing templates") 501 } 502 return template.Must(template.New(filepath.Base(filenames[0])).Funcs(templateFuncMap).ParseFiles(filenames...)) 503 } 504 } else { 505 createTemplate = func(filenames ...string) *template.Template { 506 if len(filenames) == 0 { 507 panic("Missing templates") 508 } 509 t := template.New(filepath.Base(filenames[0])).Funcs(templateFuncMap) 510 for _, filename := range filenames { 511 b, err := os.ReadFile(filename) 512 if err != nil { 513 panic(err) 514 } 515 // perform very simple minification - replace leading spaces used as formatting and new lines 516 r := regexp.MustCompile(`\n\s*`) 517 b = r.ReplaceAll(b, []byte{}) 518 s := string(b) 519 name := filepath.Base(filename) 520 var tt *template.Template 521 if name == t.Name() { 522 tt = t 523 } else { 524 tt = t.New(name) 525 } 526 _, err = tt.Parse(s) 527 if err != nil { 528 panic(err) 529 } 530 } 531 return t 532 } 533 } 534 t := make([]*template.Template, publicTplCount) 535 t[errorTpl] = createTemplate("./static/templates/error.html", "./static/templates/base.html") 536 t[errorInternalTpl] = createTemplate("./static/templates/error.html", "./static/templates/base.html") 537 t[indexTpl] = createTemplate("./static/templates/index.html", "./static/templates/base.html") 538 t[blocksTpl] = createTemplate("./static/templates/blocks.html", "./static/templates/paging.html", "./static/templates/base.html") 539 t[sendTransactionTpl] = createTemplate("./static/templates/sendtx.html", "./static/templates/base.html") 540 if s.chainParser.GetChainType() == bchain.ChainEthereumType { 541 t[txTpl] = createTemplate("./static/templates/tx.html", "./static/templates/txdetail_ethereumtype.html", "./static/templates/base.html") 542 t[addressTpl] = createTemplate("./static/templates/address.html", "./static/templates/txdetail_ethereumtype.html", "./static/templates/paging.html", "./static/templates/base.html") 543 t[blockTpl] = createTemplate("./static/templates/block.html", "./static/templates/txdetail_ethereumtype.html", "./static/templates/paging.html", "./static/templates/base.html") 544 t[nftDetailTpl] = createTemplate("./static/templates/tokenDetail.html", "./static/templates/base.html") 545 } else if s.chainParser.GetChainType() == bchain.ChainCoreCoinType { 546 t[txTpl] = createTemplate("./static/templates/tx.html", "./static/templates/txdetail_corecointype.html", "./static/templates/base.html") 547 t[addressTpl] = createTemplate("./static/templates/address.html", "./static/templates/txdetail_corecointype.html", "./static/templates/paging.html", "./static/templates/base.html") 548 t[blockTpl] = createTemplate("./static/templates/block.html", "./static/templates/txdetail_corecointype.html", "./static/templates/paging.html", "./static/templates/base.html") 549 } else { 550 t[txTpl] = createTemplate("./static/templates/tx.html", "./static/templates/txdetail.html", "./static/templates/base.html") 551 t[addressTpl] = createTemplate("./static/templates/address.html", "./static/templates/txdetail.html", "./static/templates/paging.html", "./static/templates/base.html") 552 t[blockTpl] = createTemplate("./static/templates/block.html", "./static/templates/txdetail.html", "./static/templates/paging.html", "./static/templates/base.html") 553 554 } 555 t[xpubTpl] = createTemplate("./static/templates/xpub.html", "./static/templates/txdetail.html", "./static/templates/paging.html", "./static/templates/base.html") 556 t[mempoolTpl] = createTemplate("./static/templates/mempool.html", "./static/templates/paging.html", "./static/templates/base.html") 557 return t 558 } 559 560 func (s *PublicServer) postHtmlTemplateHandler(data *TemplateData, w http.ResponseWriter, r *http.Request) { 561 // // if SecondaryCoin is specified, set secondary_coin cookie 562 if data != nil && data.SecondaryCoin != "" { 563 http.SetCookie(w, &http.Cookie{Name: secondaryCoinCookieName, Value: data.SecondaryCoin + "=" + strconv.FormatBool(data.UseSecondaryCoin), Path: "/"}) 564 } 565 566 } 567 568 func (s *PublicServer) formatAmount(a *api.Amount) string { 569 if a == nil { 570 return "0" 571 } 572 return s.chainParser.AmountToDecimalString((*big.Int)(a)) 573 } 574 575 func (s *PublicServer) amountSpan(a *api.Amount, td *TemplateData, classes string) template.HTML { 576 primary := s.formatAmount(a) 577 var rv strings.Builder 578 appendAmountWrapperSpan(&rv, primary, td.CoinShortcut, classes) 579 if s.useSatsAmountFormat { 580 appendAmountSpanBitcoinType(&rv, "prim-amt", primary, td.CoinShortcut, "") 581 } else { 582 appendAmountSpan(&rv, "prim-amt", primary, td.CoinShortcut, "") 583 } 584 if td.SecondaryCoin != "" { 585 p, err := strconv.ParseFloat(primary, 64) 586 if err == nil { 587 currentSecondary := formatSecondaryAmount(p*td.CurrentSecondaryCoinRate, td) 588 txSecondary := "" 589 // if tx is specified, compute secondary amount is at the time of tx and amount with current rate is returned with class "csec-amt" 590 if td.Tx != nil { 591 if td.TxTicker == nil { 592 date := time.Unix(td.Tx.Blocktime, 0).UTC() 593 secondary := strings.ToLower(td.SecondaryCoin) 594 var ticker *common.CurrencyRatesTicker 595 tickers, err := s.fiatRates.GetTickersForTimestamps([]int64{int64(td.Tx.Blocktime)}, "", "") 596 if err == nil && tickers != nil && len(*tickers) > 0 { 597 ticker = (*tickers)[0] 598 } 599 if ticker != nil { 600 td.TxSecondaryCoinRate = float64(ticker.Rates[secondary]) 601 // the ticker is from the midnight, valid for the whole day before 602 td.TxDate = date.Add(-1 * time.Second).Format("2006-01-02") 603 td.TxTicker = ticker 604 } 605 } 606 if td.TxSecondaryCoinRate != 0 { 607 txSecondary = formatSecondaryAmount(p*td.TxSecondaryCoinRate, td) 608 } 609 } 610 if txSecondary != "" { 611 appendAmountSpan(&rv, "sec-amt", txSecondary, td.SecondaryCoin, td.TxDate) 612 appendAmountSpan(&rv, "csec-amt", currentSecondary, td.SecondaryCoin, "") 613 } else { 614 appendAmountSpan(&rv, "sec-amt", currentSecondary, td.SecondaryCoin, "") 615 } 616 } 617 } 618 rv.WriteString("</span>") 619 return template.HTML(rv.String()) 620 } 621 622 func (s *PublicServer) amountSatsSpan(a *api.Amount, td *TemplateData, classes string) template.HTML { 623 var sats string 624 if s.chainParser.GetChainType() == bchain.ChainEthereumType || s.chainParser.GetChainType() == bchain.ChainCoreCoinType { 625 sats = a.DecimalString(9) // Gwei 626 } else { 627 sats = a.String() 628 } 629 var rv strings.Builder 630 rv.WriteString(`<span`) 631 if classes != "" { 632 rv.WriteString(` class="`) 633 rv.WriteString(classes) 634 rv.WriteString(`"`) 635 } 636 rv.WriteString(` cc="`) 637 rv.WriteString(sats) 638 rv.WriteString(`">`) 639 appendAmountSpan(&rv, "", sats, "", "") 640 rv.WriteString("</span>") 641 return template.HTML(rv.String()) 642 } 643 644 func (s *PublicServer) tokenAmountSpan(t *api.TokenTransfer, td *TemplateData, classes string) template.HTML { 645 primary := formatAmountWithDecimals(t.Value, t.Decimals) 646 var rv strings.Builder 647 appendAmountWrapperSpan(&rv, primary, t.Symbol, classes) 648 appendAmountSpan(&rv, "prim-amt", primary, t.Symbol, "") 649 if td.SecondaryCoin != "" { 650 var currentBase, currentSecondary, txBase, txSecondary string 651 p, err := strconv.ParseFloat(primary, 64) 652 if err == nil { 653 if td.CurrentTicker != nil { 654 // get rate from current ticker 655 baseRateCurrent, found := td.CurrentTicker.GetTokenRate(t.Contract) 656 if found { 657 base := p * float64(baseRateCurrent) 658 currentBase = strconv.FormatFloat(base, 'f', 6, 64) 659 currentSecondary = formatSecondaryAmount(base*td.CurrentSecondaryCoinRate, td) 660 // get the historical rate only if current rate exist 661 // it is very costly to search in DB in vain for a rate for token for which there are no exchange rates 662 baseRate, found := s.api.GetContractBaseRate(td.TxTicker, t.Contract, td.Tx.Blocktime) 663 if found { 664 base := p * baseRate 665 txBase = strconv.FormatFloat(base, 'f', 6, 64) 666 txSecondary = formatSecondaryAmount(base*td.TxSecondaryCoinRate, td) 667 } 668 } 669 } 670 } 671 if txBase != "" { 672 appendAmountSpan(&rv, "base-amt", txBase, td.CoinShortcut, td.TxDate) 673 if currentBase != "" { 674 appendAmountSpan(&rv, "cbase-amt", currentBase, td.CoinShortcut, "") 675 } 676 } else if currentBase != "" { 677 appendAmountSpan(&rv, "base-amt", currentBase, td.CoinShortcut, "") 678 } 679 if txSecondary != "" { 680 appendAmountSpan(&rv, "sec-amt", txSecondary, td.SecondaryCoin, td.TxDate) 681 if currentSecondary != "" { 682 appendAmountSpan(&rv, "csec-amt", currentSecondary, td.SecondaryCoin, "") 683 } 684 } else if currentSecondary != "" { 685 appendAmountSpan(&rv, "sec-amt", currentSecondary, td.SecondaryCoin, "") 686 } else { 687 appendAmountSpan(&rv, "sec-amt", "-", "", "") 688 } 689 } 690 rv.WriteString("</span>") 691 return template.HTML(rv.String()) 692 } 693 694 func (s *PublicServer) formattedAmountSpan(a *api.Amount, d int, symbol string, td *TemplateData, classes string) template.HTML { 695 if symbol == td.CoinShortcut { 696 d = s.chainParser.AmountDecimals() 697 } 698 value := formatAmountWithDecimals(a, d) 699 var rv strings.Builder 700 appendAmountSpan(&rv, classes, value, symbol, "") 701 return template.HTML(rv.String()) 702 } 703 704 func (s *PublicServer) summaryValuesSpan(baseValue float64, secondaryValue float64, td *TemplateData) template.HTML { 705 var rv strings.Builder 706 if secondaryValue > 0 { 707 708 appendAmountSpan(&rv, "", formatSecondaryAmount(secondaryValue, td), td.SecondaryCoin, "") 709 if baseValue > 0 && (s.chainParser.GetChainType() == bchain.ChainEthereumType || s.chainParser.GetChainType() == bchain.ChainCoreCoinType) { 710 rv.WriteString(`<span class="base-value">(`) 711 appendAmountSpan(&rv, "", strconv.FormatFloat(baseValue, 'f', 6, 64), td.CoinShortcut, "") 712 rv.WriteString(")</span>") 713 } 714 } else { 715 if baseValue > 0 { 716 appendAmountSpan(&rv, "", strconv.FormatFloat(baseValue, 'f', 6, 64), td.CoinShortcut, "") 717 } else { 718 if td.SecondaryCoin != "" { 719 rv.WriteString("-") 720 } 721 } 722 } 723 return template.HTML(rv.String()) 724 } 725 726 func formatSecondaryAmount(a float64, td *TemplateData) string { 727 if td.SecondaryCoin == "BTC" || td.SecondaryCoin == "ETH" { 728 return strconv.FormatFloat(a, 'f', 6, 64) 729 } 730 return strconv.FormatFloat(a, 'f', 2, 64) 731 } 732 733 func getAddressAlias(a string, td *TemplateData) *api.AddressAlias { 734 var alias api.AddressAlias 735 var found bool 736 if td.Block != nil { 737 alias, found = td.Block.AddressAliases[a] 738 } else if td.Address != nil { 739 alias, found = td.Address.AddressAliases[a] 740 } else if td.Tx != nil { 741 alias, found = td.Tx.AddressAliases[a] 742 } 743 if !found { 744 return nil 745 } 746 return &alias 747 } 748 749 func addressAlias(a string, td *TemplateData) string { 750 alias := getAddressAlias(a, td) 751 if alias == nil { 752 return "" 753 } 754 return alias.Alias 755 } 756 757 func addressAliasSpan(a string, td *TemplateData) template.HTML { 758 var rv strings.Builder 759 alias := getAddressAlias(a, td) 760 if alias == nil { 761 rv.WriteString(`<span class="copyable">`) 762 rv.WriteString(a) 763 } else { 764 rv.WriteString(`<span class="copyable" cc="`) 765 rv.WriteString(a) 766 rv.WriteString(`" alias-type="`) 767 rv.WriteString(alias.Type) 768 rv.WriteString(`">`) 769 rv.WriteString(alias.Alias) 770 } 771 rv.WriteString("</span>") 772 return template.HTML(rv.String()) 773 } 774 775 // called from template to support txdetail.html functionality 776 func setTxToTemplateData(td *TemplateData, tx *api.Tx) *TemplateData { 777 td.Tx = tx 778 // reset the TxTicker if different Blocktime 779 if td.TxTicker != nil && td.TxTicker.Timestamp.Unix() != tx.Blocktime { 780 td.TxSecondaryCoinRate = 0 781 td.TxTicker = nil 782 } 783 return td 784 } 785 786 // feePerByte returns fee per vByte or Byte if vsize is unknown 787 func feePerByte(tx *api.Tx) string { 788 if tx.FeesSat != nil { 789 if tx.VSize > 0 { 790 return fmt.Sprintf("%.2f sat/vByte", float64(tx.FeesSat.AsInt64())/float64(tx.VSize)) 791 } 792 if tx.Size > 0 { 793 return fmt.Sprintf("%.2f sat/Byte", float64(tx.FeesSat.AsInt64())/float64(tx.Size)) 794 } 795 } 796 return "" 797 } 798 799 // isOwnAddress returns true if the address is the one that is being shown in the explorer 800 func isOwnAddress(td *TemplateData, a string) bool { 801 return a == td.AddrStr 802 } 803 804 // called from template, returns count of token transfers of given type in a tx 805 func tokenTransfersCount(tx *api.Tx, t bchain.TokenTypeName) int { 806 count := 0 807 for i := range tx.TokenTransfers { 808 if tx.TokenTransfers[i].Type == t { 809 count++ 810 } 811 } 812 return count 813 } 814 815 // called from template, returns count of tokens in array of given type 816 func tokenCount(tokens []api.Token, t bchain.TokenTypeName) int { 817 count := 0 818 for i := range tokens { 819 if tokens[i].Type == t { 820 count++ 821 } 822 } 823 return count 824 } 825 826 func jsStr(s string) template.JSStr { 827 return template.JSStr(s) 828 } 829 830 func (s *PublicServer) explorerTx(w http.ResponseWriter, r *http.Request) (tpl, *TemplateData, error) { 831 var tx *api.Tx 832 var err error 833 s.metrics.ExplorerViews.With(common.Labels{"action": "tx"}).Inc() 834 if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 { 835 txid := r.URL.Path[i+1:] 836 tx, err = s.api.GetTransaction(txid, false, true) 837 if err != nil { 838 return errorTpl, nil, err 839 } 840 } 841 data := s.newTemplateData(r) 842 data.Tx = tx 843 return txTpl, data, nil 844 } 845 846 func (s *PublicServer) explorerSpendingTx(w http.ResponseWriter, r *http.Request) (tpl, *TemplateData, error) { 847 s.metrics.ExplorerViews.With(common.Labels{"action": "spendingtx"}).Inc() 848 var err error 849 parts := strings.Split(r.URL.Path, "/") 850 if len(parts) > 2 { 851 tx := parts[len(parts)-2] 852 n, ec := strconv.Atoi(parts[len(parts)-1]) 853 if ec == nil { 854 spendingTx, err := s.api.GetSpendingTxid(tx, n) 855 if err == nil && spendingTx != "" { 856 http.Redirect(w, r, joinURL("/tx/", spendingTx), http.StatusFound) 857 return noTpl, nil, nil 858 } 859 } 860 } 861 if err == nil { 862 err = api.NewAPIError("Transaction not found", true) 863 } 864 return errorTpl, nil, err 865 } 866 867 func (s *PublicServer) getAddressQueryParams(r *http.Request, accountDetails api.AccountDetails, maxPageSize int) (int, int, api.AccountDetails, *api.AddressFilter, string, int) { 868 var voutFilter = api.AddressFilterVoutOff 869 page, ec := strconv.Atoi(r.URL.Query().Get("page")) 870 if ec != nil { 871 page = 0 872 } 873 pageSize, ec := strconv.Atoi(r.URL.Query().Get("pageSize")) 874 if ec != nil || pageSize > maxPageSize { 875 pageSize = maxPageSize 876 } 877 from, ec := strconv.Atoi(r.URL.Query().Get("from")) 878 if ec != nil { 879 from = 0 880 } 881 to, ec := strconv.Atoi(r.URL.Query().Get("to")) 882 if ec != nil { 883 to = 0 884 } 885 filterParam := r.URL.Query().Get("filter") 886 if len(filterParam) > 0 { 887 if filterParam == "inputs" { 888 voutFilter = api.AddressFilterVoutInputs 889 } else if filterParam == "outputs" { 890 voutFilter = api.AddressFilterVoutOutputs 891 } else { 892 voutFilter, ec = strconv.Atoi(filterParam) 893 if ec != nil || voutFilter < 0 { 894 voutFilter = api.AddressFilterVoutOff 895 } 896 } 897 } 898 switch r.URL.Query().Get("details") { 899 case "basic": 900 accountDetails = api.AccountDetailsBasic 901 case "tokens": 902 accountDetails = api.AccountDetailsTokens 903 case "tokenBalances": 904 accountDetails = api.AccountDetailsTokenBalances 905 case "txids": 906 accountDetails = api.AccountDetailsTxidHistory 907 case "txslight": 908 accountDetails = api.AccountDetailsTxHistoryLight 909 case "txs": 910 accountDetails = api.AccountDetailsTxHistory 911 } 912 tokensToReturn := api.TokensToReturnNonzeroBalance 913 switch r.URL.Query().Get("tokens") { 914 case "derived": 915 tokensToReturn = api.TokensToReturnDerived 916 case "used": 917 tokensToReturn = api.TokensToReturnUsed 918 case "nonzero": 919 tokensToReturn = api.TokensToReturnNonzeroBalance 920 } 921 gap, ec := strconv.Atoi(r.URL.Query().Get("gap")) 922 if ec != nil { 923 gap = 0 924 } 925 contract := r.URL.Query().Get("contract") 926 return page, pageSize, accountDetails, &api.AddressFilter{ 927 Vout: voutFilter, 928 TokensToReturn: tokensToReturn, 929 FromHeight: uint32(from), 930 ToHeight: uint32(to), 931 Contract: contract, 932 }, filterParam, gap 933 } 934 935 func (s *PublicServer) explorerAddress(w http.ResponseWriter, r *http.Request) (tpl, *TemplateData, error) { 936 var addressParam string 937 i := strings.LastIndexByte(r.URL.Path, '/') 938 if i > 0 { 939 addressParam = r.URL.Path[i+1:] 940 } 941 if len(addressParam) == 0 { 942 return errorTpl, nil, api.NewAPIError("Missing address", true) 943 } 944 s.metrics.ExplorerViews.With(common.Labels{"action": "address"}).Inc() 945 page, _, _, filter, filterParam, _ := s.getAddressQueryParams(r, api.AccountDetailsTxHistoryLight, txsOnPage) 946 // do not allow details to be changed by query params 947 data := s.newTemplateData(r) 948 address, err := s.api.GetAddress(addressParam, page, txsOnPage, api.AccountDetailsTxHistoryLight, filter, strings.ToLower(data.SecondaryCoin)) 949 if err != nil { 950 return errorTpl, nil, err 951 } 952 data.AddrStr = address.AddrStr 953 data.Address = address 954 data.Page = address.Page 955 data.PagingRange, data.PrevPage, data.NextPage = getPagingRange(address.Page, address.TotalPages) 956 if filterParam == "" && filter.Vout > -1 { 957 filterParam = strconv.Itoa(filter.Vout) 958 } 959 if filterParam != "" { 960 data.PageParams = template.URL("&filter=" + filterParam) 961 data.Address.Filter = filterParam 962 } 963 return addressTpl, data, nil 964 } 965 966 func (s *PublicServer) explorerNftDetail(w http.ResponseWriter, r *http.Request) (tpl, *TemplateData, error) { 967 parts := strings.Split(r.URL.Path, "/") 968 if len(parts) < 3 { 969 return errorTpl, nil, api.NewAPIError("Missing parameters", true) 970 } 971 tokenId := parts[len(parts)-1] 972 contract := parts[len(parts)-2] 973 uri, ci, err := s.api.GetEthereumTokenURI(contract, tokenId) 974 s.metrics.ExplorerViews.With(common.Labels{"action": "nftDetail"}).Inc() 975 if err != nil { 976 return errorTpl, nil, api.NewAPIError(err.Error(), true) 977 } 978 if ci == nil { 979 return errorTpl, nil, api.NewAPIError(fmt.Sprintf("Unknown contract %s", contract), true) 980 } 981 data := s.newTemplateData(r) 982 data.TokenId = tokenId 983 data.ContractInfo = ci 984 data.URI = uri 985 return nftDetailTpl, data, nil 986 } 987 988 func (s *PublicServer) explorerXpub(w http.ResponseWriter, r *http.Request) (tpl, *TemplateData, error) { 989 var xpub string 990 i := strings.LastIndex(r.URL.Path, "xpub/") 991 if i > 0 { 992 xpub = r.URL.Path[i+5:] 993 } 994 if len(xpub) == 0 { 995 return errorTpl, nil, api.NewAPIError("Missing xpub", true) 996 } 997 s.metrics.ExplorerViews.With(common.Labels{"action": "xpub"}).Inc() 998 // do not allow txsOnPage and details to be changed by query params 999 page, _, _, filter, filterParam, gap := s.getAddressQueryParams(r, api.AccountDetailsTxHistoryLight, txsOnPage) 1000 data := s.newTemplateData(r) 1001 address, err := s.api.GetXpubAddress(xpub, page, txsOnPage, api.AccountDetailsTxHistoryLight, filter, gap, strings.ToLower(data.SecondaryCoin)) 1002 if err != nil { 1003 if err == api.ErrUnsupportedXpub { 1004 err = api.NewAPIError("XPUB functionality is not supported", true) 1005 } 1006 return errorTpl, nil, err 1007 } 1008 data.AddrStr = address.AddrStr 1009 data.Address = address 1010 data.Page = address.Page 1011 data.PagingRange, data.PrevPage, data.NextPage = getPagingRange(address.Page, address.TotalPages) 1012 if filterParam != "" { 1013 data.PageParams = template.URL("&filter=" + filterParam) 1014 data.Address.Filter = filterParam 1015 } 1016 data.NonZeroBalanceTokens = filter.TokensToReturn == api.TokensToReturnNonzeroBalance 1017 return xpubTpl, data, nil 1018 } 1019 1020 func (s *PublicServer) explorerBlocks(w http.ResponseWriter, r *http.Request) (tpl, *TemplateData, error) { 1021 var blocks *api.Blocks 1022 var err error 1023 s.metrics.ExplorerViews.With(common.Labels{"action": "blocks"}).Inc() 1024 page, ec := strconv.Atoi(r.URL.Query().Get("page")) 1025 if ec != nil { 1026 page = 0 1027 } 1028 blocks, err = s.api.GetBlocks(page, blocksOnPage) 1029 if err != nil { 1030 return errorTpl, nil, err 1031 } 1032 data := s.newTemplateData(r) 1033 data.Blocks = blocks 1034 data.Page = blocks.Page 1035 data.PagingRange, data.PrevPage, data.NextPage = getPagingRange(blocks.Page, blocks.TotalPages) 1036 return blocksTpl, data, nil 1037 } 1038 1039 func (s *PublicServer) explorerBlock(w http.ResponseWriter, r *http.Request) (tpl, *TemplateData, error) { 1040 var block *api.Block 1041 var err error 1042 s.metrics.ExplorerViews.With(common.Labels{"action": "block"}).Inc() 1043 if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 { 1044 page, ec := strconv.Atoi(r.URL.Query().Get("page")) 1045 if ec != nil { 1046 page = 0 1047 } 1048 block, err = s.api.GetBlock(r.URL.Path[i+1:], page, txsOnPage) 1049 if err != nil { 1050 return errorTpl, nil, err 1051 } 1052 } 1053 data := s.newTemplateData(r) 1054 data.Block = block 1055 data.Page = block.Page 1056 data.PagingRange, data.PrevPage, data.NextPage = getPagingRange(block.Page, block.TotalPages) 1057 return blockTpl, data, nil 1058 } 1059 1060 func (s *PublicServer) explorerIndex(w http.ResponseWriter, r *http.Request) (tpl, *TemplateData, error) { 1061 var si *api.SystemInfo 1062 var err error 1063 s.metrics.ExplorerViews.With(common.Labels{"action": "index"}).Inc() 1064 si, err = s.api.GetSystemInfo(false) 1065 if err != nil { 1066 return errorTpl, nil, err 1067 } 1068 data := s.newTemplateData(r) 1069 data.Info = si 1070 return indexTpl, data, nil 1071 } 1072 1073 func (s *PublicServer) explorerSearch(w http.ResponseWriter, r *http.Request) (tpl, *TemplateData, error) { 1074 q := strings.TrimSpace(r.URL.Query().Get("q")) 1075 var tx *api.Tx 1076 var address *api.Address 1077 var block *api.Block 1078 var err error 1079 s.metrics.ExplorerViews.With(common.Labels{"action": "search"}).Inc() 1080 if len(q) > 0 { 1081 address, err = s.api.GetXpubAddress(q, 0, 1, api.AccountDetailsBasic, &api.AddressFilter{Vout: api.AddressFilterVoutOff}, 0, "") 1082 if err == nil { 1083 http.Redirect(w, r, joinURL("/xpub/", url.QueryEscape(address.AddrStr)), http.StatusFound) 1084 return noTpl, nil, nil 1085 } 1086 block, err = s.api.GetBlock(q, 0, 1) 1087 if err == nil { 1088 http.Redirect(w, r, joinURL("/block/", block.Hash), http.StatusFound) 1089 return noTpl, nil, nil 1090 } 1091 tx, err = s.api.GetTransaction(q, false, false) 1092 if err == nil { 1093 http.Redirect(w, r, joinURL("/tx/", tx.Txid), http.StatusFound) 1094 return noTpl, nil, nil 1095 } 1096 address, err = s.api.GetAddress(q, 0, 1, api.AccountDetailsBasic, &api.AddressFilter{Vout: api.AddressFilterVoutOff}, "") 1097 if err == nil { 1098 http.Redirect(w, r, joinURL("/address/", address.AddrStr), http.StatusFound) 1099 return noTpl, nil, nil 1100 } 1101 } 1102 return errorTpl, nil, api.NewAPIError(fmt.Sprintf("No matching records found for '%v'", q), true) 1103 } 1104 1105 func (s *PublicServer) explorerSendTx(w http.ResponseWriter, r *http.Request) (tpl, *TemplateData, error) { 1106 s.metrics.ExplorerViews.With(common.Labels{"action": "sendtx"}).Inc() 1107 data := s.newTemplateData(r) 1108 if r.Method == http.MethodPost { 1109 err := r.ParseForm() 1110 if err != nil { 1111 return sendTransactionTpl, data, err 1112 } 1113 hex := r.FormValue("hex") 1114 if len(hex) > 0 { 1115 res, err := s.chain.SendRawTransaction(hex) 1116 if err != nil { 1117 data.SendTxHex = hex 1118 data.Error = &api.APIError{Text: err.Error(), Public: true} 1119 return sendTransactionTpl, data, nil 1120 } 1121 data.Status = "Transaction sent, result " + res 1122 } 1123 } 1124 return sendTransactionTpl, data, nil 1125 } 1126 1127 func (s *PublicServer) explorerMempool(w http.ResponseWriter, r *http.Request) (tpl, *TemplateData, error) { 1128 var mempoolTxids *api.MempoolTxids 1129 var err error 1130 s.metrics.ExplorerViews.With(common.Labels{"action": "mempool"}).Inc() 1131 page, ec := strconv.Atoi(r.URL.Query().Get("page")) 1132 if ec != nil { 1133 page = 0 1134 } 1135 mempoolTxids, err = s.api.GetMempool(page, mempoolTxsOnPage) 1136 if err != nil { 1137 return errorTpl, nil, err 1138 } 1139 data := s.newTemplateData(r) 1140 data.MempoolTxids = mempoolTxids 1141 data.Page = mempoolTxids.Page 1142 data.PagingRange, data.PrevPage, data.NextPage = getPagingRange(mempoolTxids.Page, mempoolTxids.TotalPages) 1143 return mempoolTpl, data, nil 1144 } 1145 1146 func getPagingRange(page int, total int) ([]int, int, int) { 1147 // total==-1 means total is unknown, show only prev/next buttons 1148 if total >= 0 && total < 2 { 1149 return nil, 0, 0 1150 } 1151 var r []int 1152 pp, np := page-1, page+1 1153 if pp < 1 { 1154 pp = 1 1155 } 1156 if total > 0 { 1157 if np > total { 1158 np = total 1159 } 1160 r = make([]int, 0, 8) 1161 if total < 6 { 1162 for i := 1; i <= total; i++ { 1163 r = append(r, i) 1164 } 1165 } else { 1166 r = append(r, 1) 1167 if page > 3 { 1168 r = append(r, 0) 1169 } 1170 if pp == 1 { 1171 if page == 1 { 1172 r = append(r, np) 1173 r = append(r, np+1) 1174 r = append(r, np+2) 1175 } else { 1176 r = append(r, page) 1177 r = append(r, np) 1178 r = append(r, np+1) 1179 } 1180 } else if np == total { 1181 if page == total { 1182 r = append(r, pp-2) 1183 r = append(r, pp-1) 1184 r = append(r, pp) 1185 } else { 1186 r = append(r, pp-1) 1187 r = append(r, pp) 1188 r = append(r, page) 1189 } 1190 } else { 1191 r = append(r, pp) 1192 r = append(r, page) 1193 r = append(r, np) 1194 } 1195 if page <= total-3 { 1196 r = append(r, 0) 1197 } 1198 r = append(r, total) 1199 } 1200 } 1201 return r, pp, np 1202 } 1203 1204 func (s *PublicServer) apiIndex(r *http.Request, apiVersion int) (interface{}, error) { 1205 s.metrics.ExplorerViews.With(common.Labels{"action": "api-index"}).Inc() 1206 return s.api.GetSystemInfo(false) 1207 } 1208 1209 func (s *PublicServer) apiBlockIndex(r *http.Request, apiVersion int) (interface{}, error) { 1210 type resBlockIndex struct { 1211 BlockHash string `json:"blockHash"` 1212 } 1213 var err error 1214 var hash string 1215 height := -1 1216 if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 { 1217 if h, err := strconv.Atoi(r.URL.Path[i+1:]); err == nil { 1218 height = h 1219 } 1220 } 1221 if height >= 0 { 1222 hash, err = s.db.GetBlockHash(uint32(height)) 1223 } else { 1224 _, hash, err = s.db.GetBestBlock() 1225 } 1226 if err != nil { 1227 glog.Error(err) 1228 return nil, err 1229 } 1230 return resBlockIndex{ 1231 BlockHash: hash, 1232 }, nil 1233 } 1234 1235 func (s *PublicServer) apiTx(r *http.Request, apiVersion int) (interface{}, error) { 1236 var txid string 1237 i := strings.LastIndexByte(r.URL.Path, '/') 1238 if i > 0 { 1239 txid = r.URL.Path[i+1:] 1240 } 1241 if len(txid) == 0 { 1242 return nil, api.NewAPIError("Missing txid", true) 1243 } 1244 var tx *api.Tx 1245 var err error 1246 s.metrics.ExplorerViews.With(common.Labels{"action": "api-tx"}).Inc() 1247 spendingTxs := false 1248 p := r.URL.Query().Get("spending") 1249 if len(p) > 0 { 1250 spendingTxs, err = strconv.ParseBool(p) 1251 if err != nil { 1252 return nil, api.NewAPIError("Parameter 'spending' cannot be converted to boolean", true) 1253 } 1254 } 1255 tx, err = s.api.GetTransaction(txid, spendingTxs, false) 1256 if err == nil && apiVersion == apiV1 { 1257 return s.api.TxToV1(tx), nil 1258 } 1259 return tx, err 1260 } 1261 1262 func (s *PublicServer) apiTxSpecific(r *http.Request, apiVersion int) (interface{}, error) { 1263 var txid string 1264 i := strings.LastIndexByte(r.URL.Path, '/') 1265 if i > 0 { 1266 txid = r.URL.Path[i+1:] 1267 } 1268 if len(txid) == 0 { 1269 return nil, api.NewAPIError("Missing txid", true) 1270 } 1271 var tx json.RawMessage 1272 var err error 1273 s.metrics.ExplorerViews.With(common.Labels{"action": "api-tx-specific"}).Inc() 1274 tx, err = s.chain.GetTransactionSpecific(&bchain.Tx{Txid: txid}) 1275 if err == bchain.ErrTxNotFound { 1276 return nil, api.NewAPIError(fmt.Sprintf("Transaction '%v' not found", txid), true) 1277 } 1278 return tx, err 1279 } 1280 1281 func (s *PublicServer) apiAddress(r *http.Request, apiVersion int) (interface{}, error) { 1282 var addressParam string 1283 i := strings.LastIndexByte(r.URL.Path, '/') 1284 if i > 0 { 1285 addressParam = r.URL.Path[i+1:] 1286 } 1287 if len(addressParam) == 0 { 1288 return nil, api.NewAPIError("Missing address", true) 1289 } 1290 var address *api.Address 1291 var err error 1292 s.metrics.ExplorerViews.With(common.Labels{"action": "api-address"}).Inc() 1293 page, pageSize, details, filter, _, _ := s.getAddressQueryParams(r, api.AccountDetailsTxidHistory, txsInAPI) 1294 secondaryCoin := strings.ToLower(r.URL.Query().Get("secondary")) 1295 address, err = s.api.GetAddress(addressParam, page, pageSize, details, filter, secondaryCoin) 1296 if err == nil && apiVersion == apiV1 { 1297 return s.api.AddressToV1(address), nil 1298 } 1299 return address, err 1300 } 1301 1302 func (s *PublicServer) apiXpub(r *http.Request, apiVersion int) (interface{}, error) { 1303 var xpub string 1304 i := strings.LastIndex(r.URL.Path, "xpub/") 1305 if i > 0 { 1306 xpub = r.URL.Path[i+5:] 1307 } 1308 if len(xpub) == 0 { 1309 return nil, api.NewAPIError("Missing xpub", true) 1310 } 1311 var address *api.Address 1312 var err error 1313 s.metrics.ExplorerViews.With(common.Labels{"action": "api-xpub"}).Inc() 1314 page, pageSize, details, filter, _, gap := s.getAddressQueryParams(r, api.AccountDetailsTxidHistory, txsInAPI) 1315 secondaryCoin := strings.ToLower(r.URL.Query().Get("secondary")) 1316 address, err = s.api.GetXpubAddress(xpub, page, pageSize, details, filter, gap, secondaryCoin) 1317 if err == nil && apiVersion == apiV1 { 1318 return s.api.AddressToV1(address), nil 1319 } 1320 if err == api.ErrUnsupportedXpub { 1321 err = api.NewAPIError("XPUB functionality is not supported", true) 1322 } 1323 return address, err 1324 } 1325 1326 func (s *PublicServer) apiUtxo(r *http.Request, apiVersion int) (interface{}, error) { 1327 var utxo []api.Utxo 1328 var err error 1329 if i := strings.LastIndex(r.URL.Path, "utxo/"); i > 0 { 1330 desc := r.URL.Path[i+5:] 1331 onlyConfirmed := false 1332 c := r.URL.Query().Get("confirmed") 1333 if len(c) > 0 { 1334 onlyConfirmed, err = strconv.ParseBool(c) 1335 if err != nil { 1336 return nil, api.NewAPIError("Parameter 'confirmed' cannot be converted to boolean", true) 1337 } 1338 } 1339 gap, ec := strconv.Atoi(r.URL.Query().Get("gap")) 1340 if ec != nil { 1341 gap = 0 1342 } 1343 utxo, err = s.api.GetXpubUtxo(desc, onlyConfirmed, gap) 1344 if err == nil { 1345 s.metrics.ExplorerViews.With(common.Labels{"action": "api-xpub-utxo"}).Inc() 1346 } else { 1347 utxo, err = s.api.GetAddressUtxo(desc, onlyConfirmed) 1348 s.metrics.ExplorerViews.With(common.Labels{"action": "api-address-utxo"}).Inc() 1349 } 1350 if err == nil && apiVersion == apiV1 { 1351 return s.api.AddressUtxoToV1(utxo), nil 1352 } 1353 } 1354 return utxo, err 1355 } 1356 1357 func (s *PublicServer) apiBalanceHistory(r *http.Request, apiVersion int) (interface{}, error) { 1358 var history []api.BalanceHistory 1359 var fromTimestamp, toTimestamp int64 1360 var err error 1361 if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 { 1362 gap, ec := strconv.Atoi(r.URL.Query().Get("gap")) 1363 if ec != nil { 1364 gap = 0 1365 } 1366 from := r.URL.Query().Get("from") 1367 if from != "" { 1368 fromTimestamp, err = strconv.ParseInt(from, 10, 64) 1369 if err != nil { 1370 return history, err 1371 } 1372 } 1373 to := r.URL.Query().Get("to") 1374 if to != "" { 1375 toTimestamp, err = strconv.ParseInt(to, 10, 64) 1376 if err != nil { 1377 return history, err 1378 } 1379 } 1380 var groupBy uint64 1381 groupBy, err = strconv.ParseUint(r.URL.Query().Get("groupBy"), 10, 32) 1382 if err != nil || groupBy == 0 { 1383 groupBy = 3600 1384 } 1385 fiat := r.URL.Query().Get("fiatcurrency") 1386 var fiatArray []string 1387 if fiat != "" { 1388 fiatArray = []string{fiat} 1389 } 1390 history, err = s.api.GetXpubBalanceHistory(r.URL.Path[i+1:], fromTimestamp, toTimestamp, fiatArray, gap, uint32(groupBy)) 1391 if err == nil { 1392 s.metrics.ExplorerViews.With(common.Labels{"action": "api-xpub-balancehistory"}).Inc() 1393 } else { 1394 history, err = s.api.GetBalanceHistory(r.URL.Path[i+1:], fromTimestamp, toTimestamp, fiatArray, uint32(groupBy)) 1395 s.metrics.ExplorerViews.With(common.Labels{"action": "api-address-balancehistory"}).Inc() 1396 } 1397 } 1398 return history, err 1399 } 1400 1401 func (s *PublicServer) apiBlock(r *http.Request, apiVersion int) (interface{}, error) { 1402 var block *api.Block 1403 var err error 1404 s.metrics.ExplorerViews.With(common.Labels{"action": "api-block"}).Inc() 1405 if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 { 1406 page, ec := strconv.Atoi(r.URL.Query().Get("page")) 1407 if ec != nil { 1408 page = 0 1409 } 1410 block, err = s.api.GetBlock(r.URL.Path[i+1:], page, txsInAPI) 1411 if err == nil && apiVersion == apiV1 { 1412 return s.api.BlockToV1(block), nil 1413 } 1414 } 1415 return block, err 1416 } 1417 1418 func (s *PublicServer) apiBlockRaw(r *http.Request, apiVersion int) (interface{}, error) { 1419 var block *api.BlockRaw 1420 var err error 1421 s.metrics.ExplorerViews.With(common.Labels{"action": "api-block-raw"}).Inc() 1422 if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 { 1423 block, err = s.api.GetBlockRaw(r.URL.Path[i+1:]) 1424 } 1425 return block, err 1426 } 1427 1428 func (s *PublicServer) apiFeeStats(r *http.Request, apiVersion int) (interface{}, error) { 1429 var feeStats *api.FeeStats 1430 var err error 1431 s.metrics.ExplorerViews.With(common.Labels{"action": "api-feestats"}).Inc() 1432 if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 { 1433 feeStats, err = s.api.GetFeeStats(r.URL.Path[i+1:]) 1434 } 1435 return feeStats, err 1436 } 1437 1438 type resultSendTransaction struct { 1439 Result string `json:"result"` 1440 } 1441 1442 func (s *PublicServer) apiSendTx(r *http.Request, apiVersion int) (interface{}, error) { 1443 var err error 1444 var res resultSendTransaction 1445 var hex string 1446 s.metrics.ExplorerViews.With(common.Labels{"action": "api-sendtx"}).Inc() 1447 if r.Method == http.MethodPost { 1448 data, err := io.ReadAll(r.Body) 1449 if err != nil { 1450 return nil, api.NewAPIError("Missing tx blob", true) 1451 } 1452 hex = string(data) 1453 } else { 1454 if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 { 1455 hex = r.URL.Path[i+1:] 1456 } 1457 } 1458 if len(hex) > 0 { 1459 res.Result, err = s.chain.SendRawTransaction(hex) 1460 if err != nil { 1461 return nil, api.NewAPIError(err.Error(), true) 1462 } 1463 return res, nil 1464 } 1465 return nil, api.NewAPIError("Missing tx blob", true) 1466 } 1467 1468 // apiAvailableVsCurrencies returns a list of available versus currencies 1469 func (s *PublicServer) apiAvailableVsCurrencies(r *http.Request, apiVersion int) (interface{}, error) { 1470 s.metrics.ExplorerViews.With(common.Labels{"action": "api-tickers-list"}).Inc() 1471 timestampString := strings.ToLower(r.URL.Query().Get("timestamp")) 1472 timestamp, err := strconv.ParseInt(timestampString, 10, 64) 1473 if err != nil { 1474 return nil, api.NewAPIError("Parameter \"timestamp\" is not a valid Unix timestamp.", true) 1475 } 1476 token := strings.ToLower(r.URL.Query().Get("token")) 1477 result, err := s.api.GetAvailableVsCurrencies(timestamp, token) 1478 return result, err 1479 } 1480 1481 // apiTickers returns FiatRates ticker prices for the specified block or timestamp. 1482 func (s *PublicServer) apiTickers(r *http.Request, apiVersion int) (interface{}, error) { 1483 var result *api.FiatTicker 1484 var err error 1485 1486 currency := strings.ToLower(r.URL.Query().Get("currency")) 1487 var currencies []string 1488 if currency != "" { 1489 currencies = []string{currency} 1490 } 1491 token := strings.ToLower(r.URL.Query().Get("token")) 1492 1493 if block := r.URL.Query().Get("block"); block != "" { 1494 // Get tickers for specified block height or block hash 1495 s.metrics.ExplorerViews.With(common.Labels{"action": "api-tickers-block"}).Inc() 1496 result, err = s.api.GetFiatRatesForBlockID(block, currencies, token) 1497 } else if timestampString := r.URL.Query().Get("timestamp"); timestampString != "" { 1498 // Get tickers for specified timestamp 1499 s.metrics.ExplorerViews.With(common.Labels{"action": "api-tickers-date"}).Inc() 1500 1501 timestamp, err := strconv.ParseInt(timestampString, 10, 64) 1502 if err != nil { 1503 return nil, api.NewAPIError("Parameter 'timestamp' is not a valid Unix timestamp.", true) 1504 } 1505 1506 resultTickers, err := s.api.GetFiatRatesForTimestamps([]int64{timestamp}, currencies, token) 1507 if err != nil { 1508 return nil, err 1509 } 1510 result = &resultTickers.Tickers[0] 1511 } else { 1512 // No parameters - get the latest available ticker 1513 s.metrics.ExplorerViews.With(common.Labels{"action": "api-tickers-last"}).Inc() 1514 result, err = s.api.GetCurrentFiatRates(currencies, token) 1515 } 1516 if err != nil { 1517 return nil, err 1518 } 1519 return result, nil 1520 } 1521 1522 // apiMultiTickers returns FiatRates ticker prices for the specified comma separated list of timestamps. 1523 func (s *PublicServer) apiMultiTickers(r *http.Request, apiVersion int) (interface{}, error) { 1524 var result []api.FiatTicker 1525 var err error 1526 1527 currency := strings.ToLower(r.URL.Query().Get("currency")) 1528 var currencies []string 1529 if currency != "" { 1530 currencies = []string{currency} 1531 } 1532 token := strings.ToLower(r.URL.Query().Get("token")) 1533 if timestampString := r.URL.Query().Get("timestamp"); timestampString != "" { 1534 // Get tickers for specified timestamp 1535 s.metrics.ExplorerViews.With(common.Labels{"action": "api-multi-tickers-date"}).Inc() 1536 timestamps := strings.Split(timestampString, ",") 1537 t := make([]int64, len(timestamps)) 1538 for i := range timestamps { 1539 t[i], err = strconv.ParseInt(timestamps[i], 10, 64) 1540 if err != nil { 1541 return nil, api.NewAPIError("Parameter 'timestamp' does not contain a valid Unix timestamp.", true) 1542 } 1543 } 1544 resultTickers, err := s.api.GetFiatRatesForTimestamps(t, currencies, token) 1545 if err != nil { 1546 return nil, err 1547 } 1548 result = resultTickers.Tickers 1549 } else { 1550 return nil, api.NewAPIError("Parameter 'timestamp' is missing.", true) 1551 } 1552 return result, nil 1553 } 1554 1555 type resultEstimateFeeAsString struct { 1556 Result string `json:"result"` 1557 } 1558 1559 func (s *PublicServer) apiEstimateFee(r *http.Request, apiVersion int) (interface{}, error) { 1560 var res resultEstimateFeeAsString 1561 s.metrics.ExplorerViews.With(common.Labels{"action": "api-estimatefee"}).Inc() 1562 if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 { 1563 b := r.URL.Path[i+1:] 1564 if len(b) > 0 { 1565 blocks, err := strconv.Atoi(b) 1566 if err != nil { 1567 return nil, api.NewAPIError("Parameter 'number of blocks' is not a number", true) 1568 } 1569 conservative := true 1570 c := r.URL.Query().Get("conservative") 1571 if len(c) > 0 { 1572 conservative, err = strconv.ParseBool(c) 1573 if err != nil { 1574 return nil, api.NewAPIError("Parameter 'conservative' cannot be converted to boolean", true) 1575 } 1576 } 1577 var fee big.Int 1578 fee, err = s.chain.EstimateSmartFee(blocks, conservative) 1579 if err != nil { 1580 fee, err = s.chain.EstimateFee(blocks) 1581 if err != nil { 1582 return nil, err 1583 } 1584 } 1585 res.Result = s.chainParser.AmountToDecimalString(&fee) 1586 return res, nil 1587 } 1588 } 1589 return nil, api.NewAPIError("Missing parameter 'number of blocks'", true) 1590 }