The core protocol of WoopChain
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
woop/api/service/explorer/service.go

353 lines
12 KiB

// TODO: refactor this whole module to v0 and v1
package explorer
import (
"context"
"encoding/json"
"fmt"
"math/big"
"net"
"net/http"
"path"
"strconv"
"time"
"github.com/harmony-one/harmony/core/types"
ethCommon "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/rpc"
"github.com/gorilla/mux"
msg_pb "github.com/harmony-one/harmony/api/proto/message"
"github.com/harmony-one/harmony/core"
"github.com/harmony-one/harmony/hmy"
"github.com/harmony-one/harmony/internal/chain"
"github.com/harmony-one/harmony/internal/common"
nodeconfig "github.com/harmony-one/harmony/internal/configs/node"
"github.com/harmony-one/harmony/internal/utils"
"github.com/harmony-one/harmony/numeric"
"github.com/harmony-one/harmony/p2p"
stakingReward "github.com/harmony-one/harmony/staking/reward"
)
// Constants for explorer service.
const (
explorerPortDifference = 4000
defaultPageSize = "1000"
maxAddresses = 100000
nodeSyncTolerance = 5
)
// HTTPError is an HTTP error.
type HTTPError struct {
Code int
Msg string
}
// Service is the struct for explorer service.
type Service struct {
router *mux.Router
IP string
Port string
storage *storage
server *http.Server
messageChan chan *msg_pb.Message
blockchain *core.BlockChain
backend hmy.NodeAPI
}
// New returns explorer service.
func New(selfPeer *p2p.Peer, bc *core.BlockChain, backend hmy.NodeAPI) *Service {
dbPath := defaultDBPath(selfPeer.IP, selfPeer.Port)
storage, err := newStorage(bc, dbPath)
if err != nil {
utils.Logger().Fatal().Err(err).Msg("cannot open explorer DB")
}
return &Service{
IP: selfPeer.IP,
Port: selfPeer.Port,
blockchain: bc,
backend: backend,
storage: storage,
}
}
// Start starts explorer service.
func (s *Service) Start() error {
utils.Logger().Info().Msg("Starting explorer service.")
s.Init()
s.server = s.Run()
s.storage.Start()
return nil
}
// Stop shutdowns explorer service.
func (s *Service) Stop() error {
utils.Logger().Info().Msg("Shutting down explorer service.")
if err := s.server.Shutdown(context.Background()); err != nil {
utils.Logger().Error().Err(err).Msg("Error when shutting down explorer server")
} else {
utils.Logger().Info().Msg("Shutting down explorer server successfully")
}
s.storage.Close()
return nil
}
// GetExplorerPort returns the port serving explorer dashboard. This port is explorerPortDifference less than the node port.
func GetExplorerPort(nodePort string) string {
if port, err := strconv.Atoi(nodePort); err == nil {
return fmt.Sprintf("%d", port-explorerPortDifference)
}
utils.Logger().Error().Msg("error on parsing.")
return ""
}
// Init is to initialize for ExplorerService.
func (s *Service) Init() {}
// Run is to run serving explorer.
func (s *Service) Run() *http.Server {
// Init address.
port := GetExplorerPort(s.Port)
addr := net.JoinHostPort("", port)
s.router = mux.NewRouter()
// Set up router for addresses.
// Fetch addresses request, accepts parameter size: how much addresses to read,
// parameter prefix: from which address prefix start
s.router.Path("/addresses").Queries("size", "{[0-9]*?}", "prefix", "{[a-zA-Z0-9]*?}").HandlerFunc(s.GetAddresses).Methods("GET")
s.router.Path("/addresses").HandlerFunc(s.GetAddresses)
// Set up router for supply info
s.router.Path("/burn-addresses").Queries().HandlerFunc(s.GetInaccessibleAddressInfo).Methods("GET")
s.router.Path("/burn-addresses").HandlerFunc(s.GetInaccessibleAddressInfo)
s.router.Path("/circulating-supply").Queries().HandlerFunc(s.GetCirculatingSupply).Methods("GET")
s.router.Path("/circulating-supply").HandlerFunc(s.GetCirculatingSupply)
s.router.Path("/total-supply").Queries().HandlerFunc(s.GetTotalSupply).Methods("GET")
s.router.Path("/total-supply").HandlerFunc(s.GetTotalSupply)
// Set up router for node health check.
s.router.Path("/node-sync").Queries().HandlerFunc(s.GetNodeSync).Methods("GET")
s.router.Path("/node-sync").HandlerFunc(s.GetNodeSync)
// Do serving now.
utils.Logger().Info().Str("port", GetExplorerPort(s.Port)).Msg("[Explorer] Server started.")
server := &http.Server{
Addr: addr,
Handler: s.router,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 120 * time.Second,
}
go func() {
defer func() { utils.Logger().Debug().Msg("[Explorer] Server closed.") }()
if err := server.ListenAndServe(); err != nil {
utils.Logger().Warn().Err(err).Msg("[Explorer] Server error.")
}
}()
fmt.Printf("Started Explorer server at: %v:%v\n", s.IP, port)
return server
}
// GetAddresses serves end-point /addresses, returns size of addresses from address with prefix.
func (s *Service) GetAddresses(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
sizeStr := r.FormValue("size")
prefix := r.FormValue("prefix")
if sizeStr == "" {
sizeStr = defaultPageSize
}
data := &Data{}
defer func() {
if err := json.NewEncoder(w).Encode(data.Addresses); err != nil {
utils.Logger().Warn().Err(err).Msg("cannot JSON-encode addresses")
}
}()
size, err := strconv.Atoi(sizeStr)
if err != nil || size > maxAddresses {
w.WriteHeader(http.StatusBadRequest)
return
}
data.Addresses, err = s.storage.GetAddresses(size, oneAddress(prefix))
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
utils.Logger().Warn().Err(err).Msg("wasn't able to fetch addresses from storage")
return
}
}
// GetNormalTxHashesByAccount get the normal transaction hashes by account
func (s *Service) GetNormalTxHashesByAccount(address string) ([]ethCommon.Hash, []TxType, error) {
return s.storage.GetNormalTxsByAddress(address)
}
// GetStakingTxHashesByAccount get the staking transaction hashes by account
func (s *Service) GetStakingTxHashesByAccount(address string) ([]ethCommon.Hash, []TxType, error) {
return s.storage.GetStakingTxsByAddress(address)
}
func (s *Service) GetTraceResultByHash(hash ethCommon.Hash) (json.RawMessage, error) {
return s.storage.GetTraceResultByHash(hash)
}
// DumpTraceResult instruct the explorer storage to trace data in explorer DB
func (s *Service) DumpTraceResult(hash ethCommon.Hash, data []byte) {
s.storage.DumpTraceResult(hash, data)
}
// DumpNewBlock instruct the explorer storage to dump block data in explorer DB
func (s *Service) DumpNewBlock(b *types.Block) {
s.storage.DumpNewBlock(b)
}
// DumpCatchupBlock instruct the explorer storage to dump a catch up block in explorer DB
func (s *Service) DumpCatchupBlock(b *types.Block) {
s.storage.DumpCatchupBlock(b)
}
// IsAvailable return whether the explorer db is available for now.
func (s *Service) IsAvailable() bool {
return s.storage.available.IsSet()
}
var (
// InaccessibleAddresses are a list of known eth addresses that cannot spend ONE tokens.
InaccessibleAddresses = []ethCommon.Address{
// one10000000000000000000000000000dead5shlag
ethCommon.HexToAddress("0x7bDeF7Bdef7BDeF7BDEf7bDef7bdef7bdeF6E7AD"),
}
)
// InaccessibleAddressInfo ..
type InaccessibleAddressInfo struct {
EthAddress ethCommon.Address `json:"eth-address"`
Address string `json:"address"`
Balance numeric.Dec `json:"balance"`
Nonce uint64 `json:"nonce"`
}
// getAllInaccessibleAddresses information according to InaccessibleAddresses
func (s *Service) getAllInaccessibleAddresses() ([]*InaccessibleAddressInfo, error) {
state, err := s.blockchain.StateAt(s.blockchain.CurrentHeader().Root())
if err != nil {
return nil, err
}
accs := []*InaccessibleAddressInfo{}
for _, addr := range InaccessibleAddresses {
accs = append(accs, &InaccessibleAddressInfo{
EthAddress: addr,
Address: common.MustAddressToBech32(addr),
Balance: numeric.NewDecFromBigIntWithPrec(state.GetBalance(addr), 18),
Nonce: state.GetNonce(addr),
})
}
return accs, nil
}
// getTotalInaccessibleTokens in ONE at the latest header.
func (s *Service) getTotalInaccessibleTokens() (numeric.Dec, error) {
addrInfos, err := s.getAllInaccessibleAddresses()
if err != nil {
return numeric.Dec{}, err
}
total := numeric.NewDecFromBigIntWithPrec(big.NewInt(0), 18)
for _, addr := range addrInfos {
total = total.Add(addr.Balance)
}
return total, nil
}
// GetInaccessibleAddressInfo serves /burn-addresses end-point.
func (s *Service) GetInaccessibleAddressInfo(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
accInfos, err := s.getAllInaccessibleAddresses()
if err != nil {
utils.Logger().Warn().Err(err).Msg("unable to fetch inaccessible addresses")
w.WriteHeader(http.StatusInternalServerError)
}
if err := json.NewEncoder(w).Encode(accInfos); err != nil {
utils.Logger().Warn().Msg("cannot JSON-encode inaccessible account info")
w.WriteHeader(http.StatusInternalServerError)
}
}
// GetCirculatingSupply serves /circulating-supply end-point.
// Note that known InaccessibleAddresses have their funds removed from the supply for this endpoint.
func (s *Service) GetCirculatingSupply(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
circulatingSupply, err := chain.GetCirculatingSupply(context.Background(), s.blockchain)
if err != nil {
utils.Logger().Warn().Err(err).Msg("unable to fetch circulating supply")
w.WriteHeader(http.StatusInternalServerError)
}
totalInaccessible, err := s.getTotalInaccessibleTokens()
if err != nil {
utils.Logger().Warn().Err(err).Msg("unable to fetch inaccessible tokens")
w.WriteHeader(http.StatusInternalServerError)
}
if err := json.NewEncoder(w).Encode(circulatingSupply.Sub(totalInaccessible)); err != nil {
utils.Logger().Warn().Msg("cannot JSON-encode circulating supply")
w.WriteHeader(http.StatusInternalServerError)
}
}
// GetTotalSupply serves /total-supply end-point.
// Note that known InaccessibleAddresses have their funds removed from the supply for this endpoint.
func (s *Service) GetTotalSupply(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
totalSupply, err := stakingReward.GetTotalTokens(s.blockchain)
if err != nil {
utils.Logger().Warn().Err(err).Msg("unable to fetch total supply")
w.WriteHeader(http.StatusInternalServerError)
}
totalInaccessible, err := s.getTotalInaccessibleTokens()
if err != nil {
utils.Logger().Warn().Err(err).Msg("unable to fetch inaccessible tokens")
w.WriteHeader(http.StatusInternalServerError)
}
if err := json.NewEncoder(w).Encode(totalSupply.Sub(totalInaccessible)); err != nil {
utils.Logger().Warn().Msg("cannot JSON-encode total supply")
w.WriteHeader(http.StatusInternalServerError)
}
}
// GetNodeSync returns status code Teapot 418 if node is not in sync
func (s *Service) GetNodeSync(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
sync, remote, diff := s.backend.SyncStatus(s.blockchain.ShardID())
if !sync {
utils.Logger().Debug().Uint64("remote", remote).Uint64("diff", diff).Msg("GetNodeSyncStatus")
if remote == 0 || diff > nodeSyncTolerance {
w.WriteHeader(http.StatusTeapot)
} else {
// return sync'ed if the diff is less than nodeSyncTolerance
// this tolerance is only applicable to /node-sync API call
sync = true
}
}
if err := json.NewEncoder(w).Encode(sync); err != nil {
utils.Logger().Warn().Msg("cannot JSON-encode total supply")
w.WriteHeader(http.StatusInternalServerError)
}
}
// NotifyService notify service.
func (s *Service) NotifyService(params map[string]interface{}) {}
// SetMessageChan sets up message channel to service.
func (s *Service) SetMessageChan(messageChan chan *msg_pb.Message) {
s.messageChan = messageChan
}
// APIs for the services.
func (s *Service) APIs() []rpc.API {
return nil
}
func defaultDBPath(ip, port string) string {
return path.Join(nodeconfig.GetDefaultConfig().DBDir, "explorer_storage_"+ip+"_"+port)
}