|
|
|
@ -7,31 +7,25 @@ import ( |
|
|
|
|
"math/big" |
|
|
|
|
"net" |
|
|
|
|
"net/http" |
|
|
|
|
"os" |
|
|
|
|
"sort" |
|
|
|
|
"strconv" |
|
|
|
|
"strings" |
|
|
|
|
"time" |
|
|
|
|
|
|
|
|
|
"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/consensus/reward" |
|
|
|
|
"github.com/harmony-one/harmony/internal/utils" |
|
|
|
|
"github.com/harmony-one/harmony/numeric" |
|
|
|
|
"github.com/harmony-one/harmony/p2p" |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
// Constants for explorer service.
|
|
|
|
|
const ( |
|
|
|
|
explorerPortDifference = 4000 |
|
|
|
|
paginationOffset = 10 |
|
|
|
|
txViewNone = "NONE" |
|
|
|
|
txViewAll = "ALL" |
|
|
|
|
totalSupply = 12600000000 |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
// HTTPError is an HTTP error.
|
|
|
|
@ -111,27 +105,14 @@ func (s *Service) Run() *http.Server { |
|
|
|
|
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) |
|
|
|
|
s.router.Path("/circulating-supply").Queries().HandlerFunc(s.GetCirculatingSupply).Methods("GET") |
|
|
|
|
s.router.Path("/circulating-supply").HandlerFunc(s.GetCirculatingSupply) |
|
|
|
|
|
|
|
|
|
// 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) |
|
|
|
|
// Set up router for node count.
|
|
|
|
|
s.router.Path("/total-supply").Queries().HandlerFunc(s.GetTotalSupply).Methods("GET") |
|
|
|
|
s.router.Path("/total-supply").HandlerFunc(s.GetTotalSupply) |
|
|
|
|
|
|
|
|
|
// Do serving now.
|
|
|
|
|
utils.Logger().Info().Str("port", GetExplorerPort(s.Port)).Msg("Listening") |
|
|
|
@ -144,499 +125,6 @@ func (s *Service) Run() *http.Server { |
|
|
|
|
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") |
|
|
|
@ -677,6 +165,26 @@ func (s *ServiceAPI) GetExplorerShard(ctx context.Context) *Shard { |
|
|
|
|
return &Shard{Nodes: nodes} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// GetCirculatingSupply serves /circulating-supply end-point.
|
|
|
|
|
func (s *Service) GetCirculatingSupply(w http.ResponseWriter, r *http.Request) { |
|
|
|
|
w.Header().Set("Content-Type", "application/json") |
|
|
|
|
timestamp := time.Now().Unix() |
|
|
|
|
circulatingSupply := reward.PercentageForTimeStamp(timestamp).Mul(numeric.NewDec(totalSupply)) |
|
|
|
|
if err := json.NewEncoder(w).Encode(circulatingSupply); err != nil { |
|
|
|
|
utils.Logger().Warn().Msg("cannot JSON-encode circulating supply") |
|
|
|
|
w.WriteHeader(http.StatusInternalServerError) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// GetTotalSupply serves /total-supply end-point.
|
|
|
|
|
func (s *Service) GetTotalSupply(w http.ResponseWriter, r *http.Request) { |
|
|
|
|
w.Header().Set("Content-Type", "application/json") |
|
|
|
|
if err := json.NewEncoder(w).Encode(totalSupply); 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{}) { |
|
|
|
|
return |
|
|
|
|