Skip to content

Commit

Permalink
Merge pull request #424 from multiversx/merge_rc170_into_feat_staking…
Browse files Browse the repository at this point in the history
…v4_2024.02.02

Merge rc170 into feat stakingv4 2024.02.02
  • Loading branch information
raduchis authored Feb 2, 2024
2 parents b97fb70 + 4ef0914 commit 774a2b9
Show file tree
Hide file tree
Showing 68 changed files with 2,338 additions and 1,729 deletions.
6 changes: 6 additions & 0 deletions api/errors/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ var ErrGetValueForKey = errors.New("get value for key error")
// ErrGetKeyValuePairs signals an error in getting the key-value pairs for a given address
var ErrGetKeyValuePairs = errors.New("get key value pairs error")

// ErrInvalidAddressesArray signals that an invalid input has been provided
var ErrInvalidAddressesArray = errors.New("invalid addresses array")

// ErrCannotGetAddresses signals an error when trying to fetch a bulk of accounts
var ErrCannotGetAddresses = errors.New("error while fetching a bulk of accounts")

// ErrComputeShardForAddress signals an error in computing the shard ID for a given address
var ErrComputeShardForAddress = errors.New("compute shard ID for address error")

Expand Down
25 changes: 25 additions & 0 deletions api/groups/baseAccountsGroup.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ func NewAccountsGroup(facadeHandler data.FacadeHandler) (*accountsGroup, error)
{Path: "/:address/nft/:tokenIdentifier/nonce/:nonce", Handler: ag.getESDTNftTokenData, Method: http.MethodGet},
{Path: "/:address/guardian-data", Handler: ag.getGuardianData, Method: http.MethodGet},
{Path: "/:address/is-data-trie-migrated", Handler: ag.isDataTrieMigrated, Method: http.MethodGet},
{Path: "/bulk", Handler: ag.getAccounts, Method: http.MethodPost},
}
ag.baseGroup.endpoints = baseRoutesHandlers

Expand Down Expand Up @@ -127,6 +128,30 @@ func (group *accountsGroup) getCodeHash(c *gin.Context) {
c.JSON(http.StatusOK, codeHashResponse)
}

// getAccounts will handle the request for a bulk of addresses data
func (group *accountsGroup) getAccounts(c *gin.Context) {
var addresses []string
err := c.ShouldBindJSON(&addresses)
if err != nil {
shared.RespondWithBadRequest(c, errors.ErrInvalidAddressesArray.Error())
return
}

options, err := parseAccountQueryOptions(c)
if err != nil {
shared.RespondWithValidationError(c, errors.ErrInvalidFields, err)
return
}

response, err := group.facade.GetAccounts(addresses, options)
if err != nil {
shared.RespondWithInternalError(c, errors.ErrCannotGetAddresses, err)
return
}

shared.RespondWith(c, http.StatusOK, response, "", data.ReturnCodeSuccess)
}

// getTransactions returns the transactions for the address parameter
func (group *accountsGroup) getTransactions(c *gin.Context) {
transactions, status, err := group.getTransactionsFromFacade(c)
Expand Down
99 changes: 99 additions & 0 deletions api/groups/baseAccountsGroup_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package groups_test

import (
"bytes"
"encoding/json"
"errors"
"fmt"
"net/http"
Expand Down Expand Up @@ -45,6 +47,15 @@ type balanceResponse struct {
Data balanceResponseData
}

type accountsResponseData struct {
Accounts map[string]*data.Account `json:"accounts"`
}

type accountsResponse struct {
GeneralResponse
Data accountsResponseData `json:"data"`
}

type usernameResponseData struct {
Username string `json:"username"`
}
Expand Down Expand Up @@ -240,6 +251,94 @@ func TestGetAccount_ReturnsSuccessfully(t *testing.T) {
assert.Empty(t, accountResponse.Error)
}

//------- GetAccounts

func TestGetAccount_FailsWhenInvalidRequest(t *testing.T) {
t.Parallel()

facade := &mock.FacadeStub{}
addressGroup, err := groups.NewAccountsGroup(facade)
require.NoError(t, err)
ws := startProxyServer(addressGroup, addressPath)

req, _ := http.NewRequest("POST", "/address/bulk", bytes.NewBuffer([]byte(`invalid request`)))
resp := httptest.NewRecorder()
ws.ServeHTTP(resp, req)

accountsResponse := accountsResponse{}
loadResponse(resp.Body, &accountsResponse)

assert.Equal(t, http.StatusBadRequest, resp.Code)
assert.Empty(t, accountsResponse.Data)
assert.Equal(t, accountsResponse.Error, apiErrors.ErrInvalidAddressesArray.Error())
}

func TestGetAccount_FailWhenFacadeGetAccountsFails(t *testing.T) {
t.Parallel()

returnedError := "i am an error"
facade := &mock.FacadeStub{
GetAccountsHandler: func(addresses []string, _ common.AccountQueryOptions) (*data.AccountsModel, error) {
return nil, errors.New(returnedError)
},
}
addressGroup, err := groups.NewAccountsGroup(facade)
require.NoError(t, err)
ws := startProxyServer(addressGroup, addressPath)

req, _ := http.NewRequest("POST", "/address/bulk", bytes.NewBuffer([]byte(`["test", "test"]`)))
resp := httptest.NewRecorder()
ws.ServeHTTP(resp, req)

accountsResponse := accountsResponse{}
loadResponse(resp.Body, &accountsResponse)

assert.Equal(t, http.StatusInternalServerError, resp.Code)
assert.Empty(t, accountsResponse.Data)
assert.Contains(t, accountsResponse.Error, returnedError)
}

func TestGetAccounts_ReturnsSuccessfully(t *testing.T) {
t.Parallel()

accounts := map[string]*data.Account{
"erd1alice": {
Address: "erd1alice",
Nonce: 1,
Balance: "100",
},
"erd1bob": {
Address: "erd1bob",
Nonce: 1,
Balance: "101",
},
}
facade := &mock.FacadeStub{
GetAccountsHandler: func(addresses []string, _ common.AccountQueryOptions) (*data.AccountsModel, error) {
return &data.AccountsModel{
Accounts: accounts,
}, nil
},
}
addressGroup, err := groups.NewAccountsGroup(facade)
require.NoError(t, err)
ws := startProxyServer(addressGroup, addressPath)

reqAddresses := []string{"erd1alice", "erd1bob"}
addressBytes, _ := json.Marshal(reqAddresses)
fmt.Println(string(addressBytes))
req, _ := http.NewRequest("POST", "/address/bulk", bytes.NewBuffer(addressBytes))
resp := httptest.NewRecorder()
ws.ServeHTTP(resp, req)

accountsResponse := accountsResponse{}
loadResponse(resp.Body, &accountsResponse)

assert.Equal(t, http.StatusOK, resp.Code)
assert.Equal(t, accountsResponse.Data.Accounts, accounts)
assert.Empty(t, accountsResponse.Error)
}

//------- GetBalance

func TestGetBalance_ReturnsSuccessfully(t *testing.T) {
Expand Down
5 changes: 2 additions & 3 deletions api/groups/baseNetworkGroup.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (
"github.com/multiversx/mx-chain-proxy-go/api/errors"
"github.com/multiversx/mx-chain-proxy-go/api/shared"
"github.com/multiversx/mx-chain-proxy-go/data"
"github.com/multiversx/mx-chain-proxy-go/process"
)

type networkGroup struct {
Expand Down Expand Up @@ -55,7 +54,7 @@ func NewNetworkGroup(facadeHandler data.FacadeHandler) (*networkGroup, error) {
func (group *networkGroup) getNetworkStatusData(c *gin.Context) {
shardIDUint, err := shared.FetchShardIDFromRequest(c)
if err != nil {
shared.RespondWith(c, http.StatusBadRequest, nil, process.ErrInvalidShardId.Error(), data.ReturnCodeRequestError)
shared.RespondWith(c, http.StatusBadRequest, nil, errors.ErrInvalidShardIDParam.Error(), data.ReturnCodeRequestError)
return
}

Expand Down Expand Up @@ -204,7 +203,7 @@ func (group *networkGroup) getGasConfigs(c *gin.Context) {
func (group *networkGroup) getTrieStatistics(c *gin.Context) {
shardID, err := shared.FetchShardIDFromRequest(c)
if err != nil {
shared.RespondWith(c, http.StatusBadRequest, nil, process.ErrInvalidShardId.Error(), data.ReturnCodeRequestError)
shared.RespondWith(c, http.StatusBadRequest, nil, errors.ErrInvalidShardIDParam.Error(), data.ReturnCodeRequestError)
return
}

Expand Down
1 change: 1 addition & 0 deletions api/groups/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ type AccountsFacadeHandler interface {
GetValueForKey(address string, key string, options common.AccountQueryOptions) (string, error)
GetAllESDTTokens(address string, options common.AccountQueryOptions) (*data.GenericAPIResponse, error)
GetKeyValuePairs(address string, options common.AccountQueryOptions) (*data.GenericAPIResponse, error)
GetAccounts(addresses []string, options common.AccountQueryOptions) (*data.AccountsModel, error)
GetESDTTokenData(address string, key string, options common.AccountQueryOptions) (*data.GenericAPIResponse, error)
GetESDTsWithRole(address string, role string, options common.AccountQueryOptions) (*data.GenericAPIResponse, error)
GetESDTsRoles(address string, options common.AccountQueryOptions) (*data.GenericAPIResponse, error)
Expand Down
6 changes: 6 additions & 0 deletions api/mock/facadeStub.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
type FacadeStub struct {
IsFaucetEnabledHandler func() bool
GetAccountHandler func(address string, options common.AccountQueryOptions) (*data.AccountModel, error)
GetAccountsHandler func(addresses []string, options common.AccountQueryOptions) (*data.AccountsModel, error)
GetShardIDForAddressHandler func(address string) (uint32, error)
GetValueForKeyHandler func(address string, key string, options common.AccountQueryOptions) (string, error)
GetKeyValuePairsHandler func(address string, options common.AccountQueryOptions) (*data.GenericAPIResponse, error)
Expand Down Expand Up @@ -271,6 +272,11 @@ func (f *FacadeStub) GetAccount(address string, options common.AccountQueryOptio
return f.GetAccountHandler(address, options)
}

// GetAccounts -
func (f *FacadeStub) GetAccounts(addresses []string, options common.AccountQueryOptions) (*data.AccountsModel, error) {
return f.GetAccountsHandler(addresses, options)
}

// GetKeyValuePairs -
func (f *FacadeStub) GetKeyValuePairs(address string, options common.AccountQueryOptions) (*data.GenericAPIResponse, error) {
return f.GetKeyValuePairsHandler(address, options)
Expand Down
1 change: 1 addition & 0 deletions cmd/proxy/config/apiConfig/v1_0.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ Routes = [
[APIPackages.address]
Routes = [
{ Name = "/:address", Open = true, Secured = false, RateLimit = 0 },
{ Name = "/bulk", Open = true, Secured = false, RateLimit = 0 },
{ Name = "/:address/balance", Open = true, Secured = false, RateLimit = 0 },
{ Name = "/:address/nonce", Open = true, Secured = false, RateLimit = 0 },
{ Name = "/:address/username", Open = true, Secured = false, RateLimit = 0 },
Expand Down
1 change: 1 addition & 0 deletions cmd/proxy/config/apiConfig/v_next.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ Routes = [
[APIPackages.address]
Routes = [
{ Name = "/:address", Open = true, Secured = false, RateLimit = 0 },
{ Name = "/bulk", Open = true, Secured = false, RateLimit = 0 },
{ Name = "/:address/balance", Open = true, Secured = false, RateLimit = 0 },
{ Name = "/:address/nonce", Open = true, Secured = false, RateLimit = 0 },
{ Name = "/:address/username", Open = true, Secured = false, RateLimit = 0 },
Expand Down
3 changes: 3 additions & 0 deletions cmd/proxy/config/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,11 @@
# List of Observers. If you want to define a metachain observer (needed for validator statistics route) use
# shard id 4294967295
# Fallback observers which are only used when regular ones are offline should have IsFallback = true
# Snapshotless observers are observers that can only respond to real-time requests, such as vm queries. They should have IsSnapshotless = true
[[Observers]]
ShardId = 0
Address = "http://127.0.0.1:8081"
IsSnapshotless = false

[[Observers]]
ShardId = 1
Expand All @@ -77,3 +79,4 @@
ShardId = 4294967295
Address = "http://127.0.0.1:8083"
IsFallback = false

9 changes: 9 additions & 0 deletions common/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,15 @@ type AccountQueryOptions struct {
HintEpoch core.OptionalUint32
}

// AreHistoricalCoordinatesSet returns true if historical block coordinates are set
func (a AccountQueryOptions) AreHistoricalCoordinatesSet() bool {
return a.BlockNonce.HasValue ||
a.OnStartOfEpoch.HasValue ||
a.HintEpoch.HasValue ||
len(a.BlockHash) > 0 ||
len(a.BlockRootHash) > 0
}

// BuildUrlWithAccountQueryOptions builds an URL with block query parameters
func BuildUrlWithAccountQueryOptions(path string, options AccountQueryOptions) string {
u := url.URL{Path: path}
Expand Down
38 changes: 38 additions & 0 deletions common/options_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import (
)

func TestBuildUrlWithBlockQueryOptions_ShouldWork(t *testing.T) {
t.Parallel()

builtUrl := BuildUrlWithBlockQueryOptions("/block/by-nonce/15", BlockQueryOptions{})
require.Equal(t, "/block/by-nonce/15", builtUrl)

Expand All @@ -29,6 +31,8 @@ func TestBuildUrlWithBlockQueryOptions_ShouldWork(t *testing.T) {
}

func TestBuildUrlWithAccountQueryOptions_ShouldWork(t *testing.T) {
t.Parallel()

builtUrl := BuildUrlWithAccountQueryOptions("/address/erd1alice", AccountQueryOptions{})
require.Equal(t, "/address/erd1alice", builtUrl)

Expand Down Expand Up @@ -65,6 +69,8 @@ func TestBuildUrlWithAccountQueryOptions_ShouldWork(t *testing.T) {
}

func TestBuildUrlWithAlteredAccountsQueryOptions(t *testing.T) {
t.Parallel()

resultedUrl := BuildUrlWithAlteredAccountsQueryOptions("path", GetAlteredAccountsForBlockOptions{})
require.Equal(t, "path", resultedUrl)

Expand All @@ -74,3 +80,35 @@ func TestBuildUrlWithAlteredAccountsQueryOptions(t *testing.T) {
// 2C is the ascii hex encoding of (,)
require.Equal(t, "path?tokens=token1%2Ctoken2%2Ctoken3", resultedUrl)
}

func TestAccountQueryOptions_AreHistoricalCoordinatesSet(t *testing.T) {
t.Parallel()

emptyQuery := AccountQueryOptions{}
require.False(t, emptyQuery.AreHistoricalCoordinatesSet())

queryWithNonce := AccountQueryOptions{
BlockNonce: core.OptionalUint64{HasValue: true, Value: 37},
}
require.True(t, queryWithNonce.AreHistoricalCoordinatesSet())

queryWithBlockHash := AccountQueryOptions{
BlockHash: []byte("hash"),
}
require.True(t, queryWithBlockHash.AreHistoricalCoordinatesSet())

queryWithBlockRootHash := AccountQueryOptions{
BlockRootHash: []byte("rootHash"),
}
require.True(t, queryWithBlockRootHash.AreHistoricalCoordinatesSet())

queryWithEpochStart := AccountQueryOptions{
OnStartOfEpoch: core.OptionalUint32{HasValue: true, Value: 37},
}
require.True(t, queryWithEpochStart.AreHistoricalCoordinatesSet())

queryWithHintEpoch := AccountQueryOptions{
HintEpoch: core.OptionalUint32{HasValue: true, Value: 37},
}
require.True(t, queryWithHintEpoch.AreHistoricalCoordinatesSet())
}
12 changes: 12 additions & 0 deletions data/account.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ type AccountModel struct {
BlockInfo BlockInfo `json:"blockInfo"`
}

// AccountsModel defines the model of the accounts response
type AccountsModel struct {
Accounts map[string]*Account `json:"accounts"`
}

// Account defines the data structure for an account
type Account struct {
Address string `json:"address"`
Expand Down Expand Up @@ -44,6 +49,13 @@ type AccountApiResponse struct {
Code string `json:"code"`
}

// AccountsApiResponse defines the response that will be returned by the node when requesting multiple accounts
type AccountsApiResponse struct {
Data AccountsModel `json:"data"`
Error string `json:"error"`
Code string `json:"code"`
}

// AccountKeyValueResponseData follows the format of the data field on an account key-value response
type AccountKeyValueResponseData struct {
Value string `json:"value"`
Expand Down
20 changes: 16 additions & 4 deletions data/observer.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ package data

// NodeData holds an observer data
type NodeData struct {
ShardId uint32
Address string
IsSynced bool
IsFallback bool
ShardId uint32
Address string
IsSynced bool
IsFallback bool
IsSnapshotless bool
}

// NodesReloadResponse is a DTO that holds details about nodes reloading
Expand All @@ -25,3 +26,14 @@ const (
// FullHistoryNode identifier a node that has full history mode enabled
FullHistoryNode NodeType = "full history"
)

// ObserverDataAvailabilityType represents the type to be used for the observers' data availability
type ObserverDataAvailabilityType string

const (
// AvailabilityAll mean that the observer can be used for both real-time and historical requests
AvailabilityAll ObserverDataAvailabilityType = "all"

// AvailabilityRecent means that the observer can be used only for recent data
AvailabilityRecent ObserverDataAvailabilityType = "recent"
)
Loading

0 comments on commit 774a2b9

Please sign in to comment.