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.
700 lines
19 KiB
700 lines
19 KiB
package explorer
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"math/big"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/ethereum/go-ethereum/common"
|
|
"github.com/ethereum/go-ethereum/rlp"
|
|
"github.com/ethereum/go-ethereum/rpc"
|
|
"github.com/gorilla/mux"
|
|
libp2p_peer "github.com/libp2p/go-libp2p-peer"
|
|
|
|
msg_pb "github.com/harmony-one/harmony/api/proto/message"
|
|
"github.com/harmony-one/harmony/core/types"
|
|
"github.com/harmony-one/harmony/internal/bech32"
|
|
common2 "github.com/harmony-one/harmony/internal/common"
|
|
"github.com/harmony-one/harmony/internal/utils"
|
|
"github.com/harmony-one/harmony/p2p"
|
|
)
|
|
|
|
// Constants for explorer service.
|
|
const (
|
|
explorerPortDifference = 4000
|
|
paginationOffset = 10
|
|
txViewNone = "NONE"
|
|
txViewAll = "ALL"
|
|
)
|
|
|
|
// 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
|
|
GetNodeIDs func() []libp2p_peer.ID
|
|
ShardID uint32
|
|
Storage *Storage
|
|
server *http.Server
|
|
messageChan chan *msg_pb.Message
|
|
GetAccountBalance func(common.Address) (*big.Int, error)
|
|
}
|
|
|
|
// New returns explorer service.
|
|
func New(selfPeer *p2p.Peer, shardID uint32, GetNodeIDs func() []libp2p_peer.ID, GetAccountBalance func(common.Address) (*big.Int, error)) *Service {
|
|
return &Service{
|
|
IP: selfPeer.IP,
|
|
Port: selfPeer.Port,
|
|
ShardID: shardID,
|
|
GetNodeIDs: GetNodeIDs,
|
|
GetAccountBalance: GetAccountBalance,
|
|
}
|
|
}
|
|
|
|
// ServiceAPI is rpc api.
|
|
type ServiceAPI struct {
|
|
Service *Service
|
|
}
|
|
|
|
// NewServiceAPI returns explorer service api.
|
|
func NewServiceAPI(explorerService *Service) *ServiceAPI {
|
|
return &ServiceAPI{Service: explorerService}
|
|
}
|
|
|
|
// StartService starts explorer service.
|
|
func (s *Service) StartService() {
|
|
utils.Logger().Info().Msg("Starting explorer service.")
|
|
s.Init(true)
|
|
s.server = s.Run()
|
|
}
|
|
|
|
// StopService shutdowns explorer service.
|
|
func (s *Service) StopService() {
|
|
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 successufully")
|
|
}
|
|
}
|
|
|
|
// 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(remove bool) {
|
|
s.Storage = GetStorageInstance(s.IP, s.Port, remove)
|
|
}
|
|
|
|
// Run is to run serving explorer.
|
|
func (s *Service) Run() *http.Server {
|
|
// Init address.
|
|
addr := net.JoinHostPort("", GetExplorerPort(s.Port))
|
|
|
|
s.router = mux.NewRouter()
|
|
// Set up router for blocks.
|
|
// Blocks are divided into pages in consequent groups of offset size.
|
|
s.router.Path("/blocks").Queries("from", "{[0-9]*?}", "to", "{[0-9]*?}", "page", "{[0-9]*?}", "offset", "{[0-9]*?}").HandlerFunc(s.GetExplorerBlocks).Methods("GET")
|
|
s.router.Path("/blocks").HandlerFunc(s.GetExplorerBlocks)
|
|
|
|
// Set up router for tx.
|
|
s.router.Path("/tx").Queries("id", "{[0-9A-Fa-fx]*?}").HandlerFunc(s.GetExplorerTransaction).Methods("GET")
|
|
s.router.Path("/tx").HandlerFunc(s.GetExplorerTransaction)
|
|
|
|
// Set up router for address.
|
|
// Address transactions are divided into pages in consequent groups of offset size.
|
|
s.router.Path("/address").Queries("id", fmt.Sprintf("{([0-9A-Fa-fx]*?)|(t?one1[%s]{38})}", bech32.Charset), "tx_view", "{[A-Z]*?}", "page", "{[0-9]*?}", "offset", "{[0-9]*?}").HandlerFunc(s.GetExplorerAddress).Methods("GET")
|
|
s.router.Path("/address").HandlerFunc(s.GetExplorerAddress)
|
|
|
|
// Set up router for node count.
|
|
s.router.Path("/nodes").HandlerFunc(s.GetExplorerNodeCount) // TODO(ricl): this is going to be replaced by /node-count
|
|
s.router.Path("/node-count").HandlerFunc(s.GetExplorerNodeCount)
|
|
|
|
// Set up router for shard
|
|
s.router.Path("/shard").Queries("id", "{[0-9]*?}").HandlerFunc(s.GetExplorerShard).Methods("GET")
|
|
s.router.Path("/shard").HandlerFunc(s.GetExplorerShard)
|
|
|
|
// Do serving now.
|
|
utils.Logger().Info().Str("port", GetExplorerPort(s.Port)).Msg("Listening")
|
|
server := &http.Server{Addr: addr, Handler: s.router}
|
|
go func() {
|
|
if err := server.ListenAndServe(); err != nil {
|
|
utils.Logger().Warn().Err(err).Msg("server.ListenAndServe()")
|
|
}
|
|
}()
|
|
return server
|
|
}
|
|
|
|
// ReadBlocksFromDB returns a list of types.Block to server blocks end-point.
|
|
func (s *Service) ReadBlocksFromDB(from, to int) []*types.Block {
|
|
blocks := []*types.Block{}
|
|
for i := from - 1; i <= to+1; i++ {
|
|
if i < 0 {
|
|
blocks = append(blocks, nil)
|
|
continue
|
|
}
|
|
key := GetBlockKey(i)
|
|
data, err := s.Storage.db.Get([]byte(key))
|
|
if err != nil {
|
|
blocks = append(blocks, nil)
|
|
continue
|
|
}
|
|
block := new(types.Block)
|
|
if rlp.DecodeBytes(data, block) != nil {
|
|
utils.Logger().Error().Msg("Error on getting from db")
|
|
os.Exit(1)
|
|
}
|
|
blocks = append(blocks, block)
|
|
}
|
|
return blocks
|
|
}
|
|
|
|
// GetExplorerBlocks serves end-point /blocks
|
|
func (s *Service) GetExplorerBlocks(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
from := r.FormValue("from")
|
|
to := r.FormValue("to")
|
|
pageParam := r.FormValue("page")
|
|
offsetParam := r.FormValue("offset")
|
|
order := r.FormValue("order")
|
|
data := &Data{
|
|
Blocks: []*Block{},
|
|
}
|
|
defer func() {
|
|
if err := json.NewEncoder(w).Encode(data.Blocks); err != nil {
|
|
utils.Logger().Warn().Err(err).Msg("cannot JSON-encode blocks")
|
|
}
|
|
}()
|
|
|
|
if from == "" {
|
|
utils.Logger().Warn().Msg("Missing from parameter")
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
return
|
|
}
|
|
db := s.Storage.GetDB()
|
|
fromInt, err := strconv.Atoi(from)
|
|
if err != nil {
|
|
utils.Logger().Warn().Err(err).Msg("invalid from parameter")
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
return
|
|
}
|
|
var toInt int
|
|
if to == "" {
|
|
toInt, err = func() (int, error) {
|
|
bytes, err := db.Get([]byte(BlockHeightKey))
|
|
if err == nil {
|
|
return strconv.Atoi(string(bytes))
|
|
}
|
|
return toInt, err
|
|
}()
|
|
} else {
|
|
toInt, err = strconv.Atoi(to)
|
|
}
|
|
if err != nil {
|
|
utils.Logger().Warn().Err(err).Msg("invalid to parameter")
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
return
|
|
}
|
|
var offset int
|
|
if offsetParam != "" {
|
|
offset, err = strconv.Atoi(offsetParam)
|
|
if err != nil || offset < 1 {
|
|
utils.Logger().Warn().Msg("invalid offset parameter")
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
return
|
|
}
|
|
} else {
|
|
offset = paginationOffset
|
|
}
|
|
var page int
|
|
if pageParam != "" {
|
|
page, err = strconv.Atoi(pageParam)
|
|
if err != nil {
|
|
utils.Logger().Warn().Err(err).Msg("invalid page parameter")
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
return
|
|
}
|
|
} else {
|
|
page = 0
|
|
}
|
|
|
|
accountBlocks := s.ReadBlocksFromDB(fromInt, toInt)
|
|
for id, accountBlock := range accountBlocks {
|
|
if id == 0 || id == len(accountBlocks)-1 || accountBlock == nil {
|
|
continue
|
|
}
|
|
block := NewBlock(accountBlock, id+fromInt-1)
|
|
// Populate transactions
|
|
for _, tx := range accountBlock.Transactions() {
|
|
transaction := GetTransaction(tx, accountBlock)
|
|
if transaction != nil {
|
|
block.TXs = append(block.TXs, transaction)
|
|
}
|
|
}
|
|
if accountBlocks[id-1] == nil {
|
|
block.BlockTime = int64(0)
|
|
block.PrevBlock = RefBlock{
|
|
ID: "",
|
|
Height: "",
|
|
}
|
|
} else {
|
|
block.BlockTime = accountBlock.Time().Int64() - accountBlocks[id-1].Time().Int64()
|
|
block.PrevBlock = RefBlock{
|
|
ID: accountBlocks[id-1].Hash().Hex(),
|
|
Height: strconv.Itoa(id + fromInt - 2),
|
|
}
|
|
}
|
|
if accountBlocks[id+1] == nil {
|
|
block.NextBlock = RefBlock{
|
|
ID: "",
|
|
Height: "",
|
|
}
|
|
} else {
|
|
block.NextBlock = RefBlock{
|
|
ID: accountBlocks[id+1].Hash().Hex(),
|
|
Height: strconv.Itoa(id + fromInt),
|
|
}
|
|
}
|
|
data.Blocks = append(data.Blocks, block)
|
|
}
|
|
if offset*page >= len(data.Blocks) {
|
|
data.Blocks = []*Block{}
|
|
} else if offset*page+offset > len(data.Blocks) {
|
|
data.Blocks = data.Blocks[offset*page:]
|
|
} else {
|
|
data.Blocks = data.Blocks[offset*page : offset*page+offset]
|
|
}
|
|
if order == "DESC" {
|
|
sort.Slice(data.Blocks[:], func(i, j int) bool {
|
|
return data.Blocks[i].Timestamp > data.Blocks[j].Timestamp
|
|
})
|
|
} else {
|
|
sort.Slice(data.Blocks[:], func(i, j int) bool {
|
|
return data.Blocks[i].Timestamp < data.Blocks[j].Timestamp
|
|
})
|
|
}
|
|
}
|
|
|
|
// GetExplorerBlocks rpc end-point.
|
|
func (s *ServiceAPI) GetExplorerBlocks(ctx context.Context, from, to, page, offset int, order string) ([]*Block, error) {
|
|
if offset == 0 {
|
|
offset = paginationOffset
|
|
}
|
|
db := s.Service.Storage.GetDB()
|
|
if to == 0 {
|
|
bytes, err := db.Get([]byte(BlockHeightKey))
|
|
if err == nil {
|
|
to, err = strconv.Atoi(string(bytes))
|
|
if err != nil {
|
|
utils.Logger().Info().Msg("failed to fetch block height")
|
|
return nil, err
|
|
}
|
|
}
|
|
}
|
|
blocks := make([]*Block, 0)
|
|
accountBlocks := s.Service.ReadBlocksFromDB(from, to)
|
|
for id, accountBlock := range accountBlocks {
|
|
if id == 0 || id == len(accountBlocks)-1 || accountBlock == nil {
|
|
continue
|
|
}
|
|
block := NewBlock(accountBlock, id+from-1)
|
|
// Populate transactions
|
|
for _, tx := range accountBlock.Transactions() {
|
|
transaction := GetTransaction(tx, accountBlock)
|
|
if transaction != nil {
|
|
block.TXs = append(block.TXs, transaction)
|
|
}
|
|
}
|
|
if accountBlocks[id-1] == nil {
|
|
block.BlockTime = int64(0)
|
|
block.PrevBlock = RefBlock{
|
|
ID: "",
|
|
Height: "",
|
|
}
|
|
} else {
|
|
block.BlockTime = accountBlock.Time().Int64() - accountBlocks[id-1].Time().Int64()
|
|
block.PrevBlock = RefBlock{
|
|
ID: accountBlocks[id-1].Hash().Hex(),
|
|
Height: strconv.Itoa(id + from - 2),
|
|
}
|
|
}
|
|
if accountBlocks[id+1] == nil {
|
|
block.NextBlock = RefBlock{
|
|
ID: "",
|
|
Height: "",
|
|
}
|
|
} else {
|
|
block.NextBlock = RefBlock{
|
|
ID: accountBlocks[id+1].Hash().Hex(),
|
|
Height: strconv.Itoa(id + from),
|
|
}
|
|
}
|
|
blocks = append(blocks, block)
|
|
}
|
|
if offset*page >= len(blocks) {
|
|
blocks = []*Block{}
|
|
} else if offset*page+offset > len(blocks) {
|
|
blocks = blocks[offset*page:]
|
|
} else {
|
|
blocks = blocks[offset*page : offset*page+offset]
|
|
}
|
|
if order == "DESC" {
|
|
sort.Slice(blocks[:], func(i, j int) bool {
|
|
return blocks[i].Timestamp > blocks[j].Timestamp
|
|
})
|
|
} else {
|
|
sort.Slice(blocks[:], func(i, j int) bool {
|
|
return blocks[i].Timestamp < blocks[j].Timestamp
|
|
})
|
|
}
|
|
return blocks, nil
|
|
}
|
|
|
|
// GetExplorerTransaction servers /tx end-point.
|
|
func (s *Service) GetExplorerTransaction(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
id := r.FormValue("id")
|
|
|
|
data := &Data{}
|
|
defer func() {
|
|
if err := json.NewEncoder(w).Encode(data.TX); err != nil {
|
|
utils.Logger().Warn().Err(err).Msg("cannot JSON-encode TX")
|
|
}
|
|
}()
|
|
if id == "" {
|
|
utils.Logger().Warn().Msg("invalid id parameter")
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
return
|
|
}
|
|
db := s.Storage.GetDB()
|
|
bytes, err := db.Get([]byte(GetTXKey(id)))
|
|
if err != nil {
|
|
utils.Logger().Warn().Err(err).Str("id", id).Msg("cannot read TX")
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
return
|
|
}
|
|
tx := new(Transaction)
|
|
if rlp.DecodeBytes(bytes, tx) != nil {
|
|
utils.Logger().Warn().Str("id", id).Msg("cannot convert data from DB")
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
return
|
|
}
|
|
data.TX = *tx
|
|
}
|
|
|
|
// GetExplorerTransaction rpc end-point.
|
|
func (s *ServiceAPI) GetExplorerTransaction(ctx context.Context, id string) (*Transaction, error) {
|
|
if id == "" {
|
|
utils.Logger().Warn().Msg("invalid id parameter")
|
|
return nil, nil
|
|
}
|
|
db := s.Service.Storage.GetDB()
|
|
bytes, err := db.Get([]byte(GetTXKey(id)))
|
|
if err != nil {
|
|
utils.Logger().Warn().Err(err).Str("id", id).Msg("cannot read TX")
|
|
return nil, err
|
|
}
|
|
tx := new(Transaction)
|
|
if rlp.DecodeBytes(bytes, tx) != nil {
|
|
utils.Logger().Warn().Str("id", id).Msg("cannot convert data from DB")
|
|
return nil, err
|
|
}
|
|
return tx, nil
|
|
}
|
|
|
|
// GetExplorerAddress serves /address end-point.
|
|
func (s *Service) GetExplorerAddress(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
id := r.FormValue("id")
|
|
if strings.HasPrefix(id, "0x") {
|
|
parsedAddr := common2.ParseAddr(id)
|
|
oneAddr, err := common2.AddressToBech32(parsedAddr)
|
|
if err != nil {
|
|
utils.Logger().Warn().Err(err).Msg("unrecognized address format")
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
return
|
|
}
|
|
id = oneAddr
|
|
}
|
|
key := GetAddressKey(id)
|
|
txViewParam := r.FormValue("tx_view")
|
|
pageParam := r.FormValue("page")
|
|
offsetParam := r.FormValue("offset")
|
|
order := r.FormValue("order")
|
|
txView := txViewNone
|
|
if txViewParam != "" {
|
|
txView = txViewParam
|
|
}
|
|
utils.Logger().Info().Str("Address", id).Msg("Querying address")
|
|
data := &Data{}
|
|
defer func() {
|
|
if err := json.NewEncoder(w).Encode(data.Address); err != nil {
|
|
utils.Logger().Warn().Err(err).Msg("cannot JSON-encode Address")
|
|
}
|
|
}()
|
|
if id == "" {
|
|
utils.Logger().Warn().Msg("missing address id param")
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
return
|
|
}
|
|
var err error
|
|
var offset int
|
|
if offsetParam != "" {
|
|
offset, err = strconv.Atoi(offsetParam)
|
|
if err != nil || offset < 1 {
|
|
utils.Logger().Warn().Msg("invalid offset parameter")
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
return
|
|
}
|
|
} else {
|
|
offset = paginationOffset
|
|
}
|
|
var page int
|
|
if pageParam != "" {
|
|
page, err = strconv.Atoi(pageParam)
|
|
if err != nil {
|
|
utils.Logger().Warn().Err(err).Msg("invalid page parameter")
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
return
|
|
}
|
|
} else {
|
|
page = 0
|
|
}
|
|
|
|
data.Address.ID = id
|
|
// Try to populate the banace by directly calling get balance.
|
|
// Check the balance from blockchain rather than local DB dump
|
|
balanceAddr := big.NewInt(0)
|
|
if s.GetAccountBalance != nil {
|
|
address := common2.ParseAddr(id)
|
|
balance, err := s.GetAccountBalance(address)
|
|
if err == nil {
|
|
balanceAddr = balance
|
|
}
|
|
}
|
|
|
|
db := s.Storage.GetDB()
|
|
bytes, err := db.Get([]byte(key))
|
|
if err != nil {
|
|
utils.Logger().Warn().Err(err).Str("id", id).Msg("cannot fetch address from db")
|
|
data.Address.Balance = balanceAddr
|
|
return
|
|
}
|
|
|
|
if err = rlp.DecodeBytes(bytes, &data.Address); err != nil {
|
|
utils.Logger().Warn().Str("id", id).Msg("cannot convert address data")
|
|
data.Address.Balance = balanceAddr
|
|
return
|
|
}
|
|
|
|
data.Address.Balance = balanceAddr
|
|
|
|
switch txView {
|
|
case txViewNone:
|
|
data.Address.TXs = nil
|
|
case Received:
|
|
receivedTXs := make([]*Transaction, 0)
|
|
for _, tx := range data.Address.TXs {
|
|
if tx.Type == Received {
|
|
receivedTXs = append(receivedTXs, tx)
|
|
}
|
|
}
|
|
data.Address.TXs = receivedTXs
|
|
case Sent:
|
|
sentTXs := make([]*Transaction, 0)
|
|
for _, tx := range data.Address.TXs {
|
|
if tx.Type == Sent {
|
|
sentTXs = append(sentTXs, tx)
|
|
}
|
|
}
|
|
data.Address.TXs = sentTXs
|
|
}
|
|
if offset*page >= len(data.Address.TXs) {
|
|
data.Address.TXs = []*Transaction{}
|
|
} else if offset*page+offset > len(data.Address.TXs) {
|
|
data.Address.TXs = data.Address.TXs[offset*page:]
|
|
} else {
|
|
data.Address.TXs = data.Address.TXs[offset*page : offset*page+offset]
|
|
}
|
|
if order == "DESC" {
|
|
sort.Slice(data.Address.TXs[:], func(i, j int) bool {
|
|
return data.Address.TXs[i].Timestamp > data.Address.TXs[j].Timestamp
|
|
})
|
|
} else {
|
|
sort.Slice(data.Address.TXs[:], func(i, j int) bool {
|
|
return data.Address.TXs[i].Timestamp < data.Address.TXs[j].Timestamp
|
|
})
|
|
}
|
|
}
|
|
|
|
// GetExplorerAddress rpc end-point.
|
|
func (s *ServiceAPI) GetExplorerAddress(ctx context.Context, id, txView string, page, offset int, order string) (*Address, error) {
|
|
if strings.HasPrefix(id, "0x") {
|
|
parsedAddr := common2.ParseAddr(id)
|
|
oneAddr, err := common2.AddressToBech32(parsedAddr)
|
|
if err != nil {
|
|
utils.Logger().Warn().Msg("unrecognized address format")
|
|
return nil, err
|
|
}
|
|
id = oneAddr
|
|
}
|
|
if offset == 0 {
|
|
offset = paginationOffset
|
|
}
|
|
if txView == "" {
|
|
txView = txViewNone
|
|
}
|
|
utils.Logger().Info().Str("Address", id).Msg("Querying address")
|
|
if id == "" {
|
|
utils.Logger().Warn().Msg("missing address id param")
|
|
return nil, nil
|
|
}
|
|
address := &Address{}
|
|
address.ID = id
|
|
// Try to populate the banace by directly calling get balance.
|
|
// Check the balance from blockchain rather than local DB dump
|
|
balanceAddr := big.NewInt(0)
|
|
if s.Service.GetAccountBalance != nil {
|
|
addr := common2.ParseAddr(id)
|
|
balance, err := s.Service.GetAccountBalance(addr)
|
|
if err == nil {
|
|
balanceAddr = balance
|
|
}
|
|
}
|
|
|
|
key := GetAddressKey(id)
|
|
db := s.Service.Storage.GetDB()
|
|
bytes, err := db.Get([]byte(key))
|
|
if err != nil {
|
|
utils.Logger().Warn().Err(err).Str("id", id).Msg("cannot fetch address from db")
|
|
address.Balance = balanceAddr
|
|
return address, err
|
|
}
|
|
if err = rlp.DecodeBytes(bytes, &address); err != nil {
|
|
utils.Logger().Warn().Str("id", id).Msg("cannot convert address data")
|
|
address.Balance = balanceAddr
|
|
return address, err
|
|
}
|
|
|
|
address.Balance = balanceAddr
|
|
|
|
switch txView {
|
|
case txViewNone:
|
|
address.TXs = nil
|
|
case Received:
|
|
receivedTXs := make([]*Transaction, 0)
|
|
for _, tx := range address.TXs {
|
|
if tx.Type == Received {
|
|
receivedTXs = append(receivedTXs, tx)
|
|
}
|
|
}
|
|
address.TXs = receivedTXs
|
|
case Sent:
|
|
sentTXs := make([]*Transaction, 0)
|
|
for _, tx := range address.TXs {
|
|
if tx.Type == Sent {
|
|
sentTXs = append(sentTXs, tx)
|
|
}
|
|
}
|
|
address.TXs = sentTXs
|
|
}
|
|
|
|
if offset*page >= len(address.TXs) {
|
|
address.TXs = []*Transaction{}
|
|
} else if offset*page+offset > len(address.TXs) {
|
|
address.TXs = address.TXs[offset*page:]
|
|
} else {
|
|
address.TXs = address.TXs[offset*page : offset*page+offset]
|
|
}
|
|
if order == "DESC" {
|
|
sort.Slice(address.TXs[:], func(i, j int) bool {
|
|
return address.TXs[i].Timestamp > address.TXs[j].Timestamp
|
|
})
|
|
} else {
|
|
sort.Slice(address.TXs[:], func(i, j int) bool {
|
|
return address.TXs[i].Timestamp < address.TXs[j].Timestamp
|
|
})
|
|
}
|
|
return address, nil
|
|
}
|
|
|
|
// GetExplorerNodeCount serves /nodes end-point.
|
|
func (s *Service) GetExplorerNodeCount(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
if err := json.NewEncoder(w).Encode(len(s.GetNodeIDs())); err != nil {
|
|
utils.Logger().Warn().Msg("cannot JSON-encode node count")
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
// GetExplorerNodeCount rpc end-point.
|
|
func (s *ServiceAPI) GetExplorerNodeCount(ctx context.Context) int {
|
|
return len(s.Service.GetNodeIDs())
|
|
}
|
|
|
|
// GetExplorerShard serves /shard end-point.
|
|
func (s *Service) GetExplorerShard(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
var nodes []Node
|
|
for _, nodeID := range s.GetNodeIDs() {
|
|
nodes = append(nodes, Node{
|
|
ID: libp2p_peer.IDB58Encode(nodeID),
|
|
})
|
|
}
|
|
if err := json.NewEncoder(w).Encode(Shard{Nodes: nodes}); err != nil {
|
|
utils.Logger().Warn().Msg("cannot JSON-encode shard info")
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
// GetExplorerShard rpc end-point.
|
|
func (s *ServiceAPI) GetExplorerShard(ctx context.Context) *Shard {
|
|
var nodes []Node
|
|
for _, nodeID := range s.Service.GetNodeIDs() {
|
|
nodes = append(nodes, Node{
|
|
ID: libp2p_peer.IDB58Encode(nodeID),
|
|
})
|
|
}
|
|
return &Shard{Nodes: nodes}
|
|
}
|
|
|
|
// NotifyService notify service.
|
|
func (s *Service) NotifyService(params map[string]interface{}) {
|
|
return
|
|
}
|
|
|
|
// 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 []rpc.API{
|
|
{
|
|
Namespace: "explorer",
|
|
Version: "1.0",
|
|
Service: NewServiceAPI(s),
|
|
Public: true,
|
|
},
|
|
}
|
|
}
|
|
|