Skip to content

Commit

Permalink
Merge pull request #157 from ElrondNetwork/merge-dev-in-master
Browse files Browse the repository at this point in the history
Merge dev in master
  • Loading branch information
miiu96 authored Feb 24, 2021
2 parents d9e4b22 + 85c3f41 commit 551567d
Show file tree
Hide file tree
Showing 63 changed files with 1,871 additions and 126 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ For more details, go to [docs.elrond.com](https://docs.elrond.com/sdk-and-tools/
- `/v1.0/address/:address/balance` (GET) --> returns the balance of a given :address.
- `/v1.0/address/:address/nonce` (GET) --> returns the nonce of an :address.
- `/v1.0/address/:address/shard` (GET) --> returns the shard of an :address based on current proxy's configuration.
- `/v1.0/address/:address/keys ` (GET) --> returns the key-value pairs of an :address.
- `/v1.0/address/:address/storage/:key` (GET) --> returns the value for a given key for an account.
- `/v1.0/address/:address/transactions` (GET) --> returns the transactions stored in indexer for a given :address.
- `/v1.0/address/:address/esdt` (GET) --> returns the account's ESDT tokens list for the given :address.
Expand All @@ -25,6 +26,7 @@ For more details, go to [docs.elrond.com](https://docs.elrond.com/sdk-and-tools/

- `/v1.0/transaction/send` (POST) --> receives a single transaction in JSON format and forwards it to an observer in the same shard as the sender's shard ID. Returns the transaction's hash if successful or the interceptor error otherwise.
- `/v1.0/transaction/simulate` (POST) --> same as /transaction/send but does not execute it. will output simulation results
- `/v1.0/transaction/simulate?checkSignature=false` (POST) --> same as /transaction/send but does not execute it, also the signature of the transaction will not be verified. will output simulation results
- `/v1.0/transaction/send-multiple` (POST) --> receives a bulk of transactions in JSON format and will forward them to observers in the rights shards. Will return the number of transactions which were accepted by the interceptor and forwarded on the p2p topic.
- `/v1.0/transaction/send-user-funds` (POST) --> receives a request containing `address`, `numOfTxs` and `value` and will select a random account from the PEM file in the same shard as the address received. Will return the transaction's hash if successful or the interceptor error otherwise.
- `/v1.0/transaction/cost` (POST) --> receives a single transaction in JSON format and returns it's cost
Expand Down
95 changes: 90 additions & 5 deletions api/api.go
Original file line number Diff line number Diff line change
@@ -1,24 +1,39 @@
package api

import (
"encoding/hex"
"fmt"
"net/http"
"reflect"
"time"

logger "github.com/ElrondNetwork/elrond-go-logger"
"github.com/ElrondNetwork/elrond-go/hashing"
"github.com/ElrondNetwork/elrond-go/hashing/factory"
"github.com/ElrondNetwork/elrond-go/hashing/sha256"
"github.com/ElrondNetwork/elrond-proxy-go/api/middleware"
"github.com/ElrondNetwork/elrond-proxy-go/config"
"github.com/ElrondNetwork/elrond-proxy-go/data"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"gopkg.in/go-playground/validator.v8"
)

var log = logger.GetOrCreate("api")

type validatorInput struct {
Name string
Validator validator.Func
}

// CreateServer creates a HTTP server
func CreateServer(versionsRegistry data.VersionsRegistryHandler, port int) (*http.Server, error) {
func CreateServer(
versionsRegistry data.VersionsRegistryHandler,
port int,
apiLoggingConfig config.ApiLoggingConfig,
credentialsConfig config.CredentialsConfig,
) (*http.Server, error) {
ws := gin.Default()
ws.Use(cors.Default())

Expand All @@ -27,7 +42,7 @@ func CreateServer(versionsRegistry data.VersionsRegistryHandler, port int) (*htt
return nil, err
}

err = registerRoutes(ws, versionsRegistry)
err = registerRoutes(ws, versionsRegistry, apiLoggingConfig, credentialsConfig)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -55,24 +70,94 @@ func registerValidators() error {
return nil
}

func registerRoutes(ws *gin.Engine, versionsRegistry data.VersionsRegistryHandler) error {
func registerRoutes(
ws *gin.Engine,
versionsRegistry data.VersionsRegistryHandler,
apiLoggingConfig config.ApiLoggingConfig,
credentialsConfig config.CredentialsConfig,
) error {
versionsMap, err := versionsRegistry.GetAllVersions()
if err != nil {
return err
}

if apiLoggingConfig.LoggingEnabled {
responseLoggerMiddleware := middleware.NewResponseLoggerMiddleware(time.Duration(apiLoggingConfig.ThresholdInMicroSeconds) * time.Microsecond)
ws.Use(responseLoggerMiddleware.MiddlewareHandlerFunc())
}

for version, versionData := range versionsMap {
versionGroup := ws.Group(version)

for path, group := range versionData.ApiHandler.GetAllGroups() {
subGroup := versionGroup.Group(path)
group.RegisterRoutes(subGroup)
group.RegisterRoutes(subGroup, versionData.ApiConfig, getAuthenticationFunc(credentialsConfig))
}
}

return nil
}

func getAuthenticationFunc(credentialsConfig config.CredentialsConfig) gin.HandlerFunc {
if len(credentialsConfig.Credentials) == 0 {
return func(c *gin.Context) {
c.AbortWithStatusJSON(
http.StatusInternalServerError,
data.GenericAPIResponse{
Data: nil,
Error: "no credentials found on server",
Code: data.ReturnCodeInternalError,
},
)
}
}

var hasher hashing.Hasher
var err error
hasher, err = factory.NewHasher(credentialsConfig.Hasher.Type)
if err != nil {
log.Warn("cannot create hasher from config. Will use Sha256 as default", "error", err)
hasher = sha256.Sha256{} // fallback in case the hasher creation failed
}

accounts := gin.Accounts{}
for _, pair := range credentialsConfig.Credentials {
accounts[pair.Username] = pair.Password
}

authenticationFunction := func(c *gin.Context) {
user, pass, ok := c.Request.BasicAuth()
if !ok {
c.AbortWithStatusJSON(http.StatusUnauthorized, data.GenericAPIResponse{
Data: nil,
Error: "this endpoint requires Basic Authentication",
Code: data.ReturnCodeRequestError,
})
return
}

userPassword, ok := accounts[user]
if !ok {
c.AbortWithStatusJSON(http.StatusUnauthorized, data.GenericAPIResponse{
Data: nil,
Error: "username does not exist",
Code: data.ReturnCodeRequestError,
})
return
}

if userPassword != hex.EncodeToString(hasher.Compute(pass)) {
c.AbortWithStatusJSON(http.StatusUnauthorized, data.GenericAPIResponse{
Data: nil,
Error: "invalid password",
Code: data.ReturnCodeRequestError,
})
return
}
}

return authenticationFunction
}

// skValidator validates a secret key from user input for correctness
func skValidator(
_ *validator.Validate,
Expand Down
6 changes: 6 additions & 0 deletions api/apiHandler.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ func initBaseGroupsWithFacade(facade data.FacadeHandler) (map[string]data.GroupH
return nil, err
}

actionsGroup, err := groups.NewActionsGroup(facade)
if err != nil {
return nil, err
}

blocksGroup, err := groups.NewBlockGroup(facade)
if err != nil {
return nil, err
Expand Down Expand Up @@ -75,6 +80,7 @@ func initBaseGroupsWithFacade(facade data.FacadeHandler) (map[string]data.GroupH
}

return map[string]data.GroupHandler{
"/actions": actionsGroup,
"/address": accountsGroup,
"/block": blocksGroup,
"/block-atlas": blockAtlasGroup,
Expand Down
9 changes: 6 additions & 3 deletions api/errors/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@ var ErrInvalidAppContext = errors.New("invalid app context")
// ErrGetValueForKey signals an error in getting the value of a key for an account
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")

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

// ErrGetESDTTokenData signals an error in fetching an ESDT token data
var ErrGetESDTTokenData = errors.New("cannot get ESDT token data")

// ErrGetAllESDTTokens signals an error in fetching all ESDT tokens for an address
var ErrGetAllESDTTokens = errors.New("cannot get all ESDT tokens")

// ErrEmptyAddress signals that an empty address was provided
var ErrEmptyAddress = errors.New("address is empty")

Expand All @@ -44,6 +44,9 @@ var ErrValidation = errors.New("validation error")
// ErrValidationQueryParameterWithResult signals that an invalid query parameter has been provided
var ErrValidationQueryParameterWithResult = errors.New("invalid query parameter withResults")

// ErrValidatorQueryParameterCheckSignature signals that an invalid query parameter has been provided
var ErrValidatorQueryParameterCheckSignature = errors.New("invalid query parameter checkSignature")

// ErrInvalidSignatureHex signals a wrong hex value was provided for the signature
var ErrInvalidSignatureHex = errors.New("invalid signature, could not decode hex value")

Expand Down
24 changes: 24 additions & 0 deletions api/groups/baseAccountsGroup.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ func NewAccountsGroup(facadeHandler data.FacadeHandler) (*accountsGroup, error)
"/:address/nonce": {Handler: ag.getNonce, Method: http.MethodGet},
"/:address/shard": {Handler: ag.getShard, Method: http.MethodGet},
"/:address/transactions": {Handler: ag.getTransactions, Method: http.MethodGet},
"/:address/keys": {Handler: ag.getKeyValuePairs, Method: http.MethodGet},
"/:address/key/:key": {Handler: ag.getValueForKey, Method: http.MethodGet},
"/:address/esdt": {Handler: ag.getESDTTokens, Method: http.MethodGet},
"/:address/esdt/:tokenIdentifier": {Handler: ag.getESDTTokenData, Method: http.MethodGet},
Expand Down Expand Up @@ -119,6 +120,29 @@ func (group *accountsGroup) getTransactions(c *gin.Context) {
shared.RespondWith(c, http.StatusOK, gin.H{"transactions": transactions}, "", data.ReturnCodeSuccess)
}

// getKeyValuePairs returns the key-value pairs for the address parameter
func (group *accountsGroup) getKeyValuePairs(c *gin.Context) {
addr := c.Param("address")
if addr == "" {
shared.RespondWith(
c,
http.StatusBadRequest,
nil,
fmt.Sprintf("%v: %v", errors.ErrGetKeyValuePairs, errors.ErrEmptyAddress),
data.ReturnCodeRequestError,
)
return
}

keyValuePairs, err := group.facade.GetKeyValuePairs(addr)
if err != nil {
shared.RespondWith(c, http.StatusInternalServerError, nil, err.Error(), data.ReturnCodeInternalError)
return
}

c.JSON(http.StatusOK, keyValuePairs)
}

// getValueForKey returns the value for the given address and key
func (group *accountsGroup) getValueForKey(c *gin.Context) {
addr := c.Param("address")
Expand Down
60 changes: 60 additions & 0 deletions api/groups/baseAccountsGroup_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -434,3 +434,63 @@ func TestGetESDTTokenData_ReturnsSuccessfully(t *testing.T) {
assert.Equal(t, shardResponse.Data.TokenData, expectedTokenData)
assert.Empty(t, shardResponse.Error)
}

// ---- GetKeyValuePairs

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

expectedErr := errors.New("internal err")
facade := &mock.Facade{
GetKeyValuePairsHandler: func(_ string) (*data.GenericAPIResponse, error) {
return nil, expectedErr
},
}
addressGroup, err := groups.NewAccountsGroup(facade)
require.NoError(t, err)
ws := startProxyServer(addressGroup, addressPath)

reqAddress := "test"
req, _ := http.NewRequest("GET", fmt.Sprintf("/address/%s/keys", reqAddress), nil)
resp := httptest.NewRecorder()
ws.ServeHTTP(resp, req)

response := &data.GenericAPIResponse{}
loadResponse(resp.Body, &response)

assert.Equal(t, http.StatusInternalServerError, resp.Code)
assert.True(t, strings.Contains(response.Error, expectedErr.Error()))
}

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

expectedResponse := &data.GenericAPIResponse{
Data: map[string]interface{}{"pairs": map[string]interface{}{
"key1": "value1",
"key2": "value2",
}},
Error: "",
Code: data.ReturnCodeSuccess,
}
facade := &mock.Facade{
GetKeyValuePairsHandler: func(_ string) (*data.GenericAPIResponse, error) {
return expectedResponse, nil
},
}
addressGroup, err := groups.NewAccountsGroup(facade)
require.NoError(t, err)
ws := startProxyServer(addressGroup, addressPath)

reqAddress := "test"
req, _ := http.NewRequest("GET", fmt.Sprintf("/address/%s/keys", reqAddress), nil)
resp := httptest.NewRecorder()
ws.ServeHTTP(resp, req)

actualResponse := &data.GenericAPIResponse{}
loadResponse(resp.Body, &actualResponse)

assert.Equal(t, http.StatusOK, resp.Code)
assert.Equal(t, expectedResponse, actualResponse)
assert.Empty(t, actualResponse.Error)
}
61 changes: 61 additions & 0 deletions api/groups/baseActionsGroup.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package groups

import (
"net/http"

"github.com/ElrondNetwork/elrond-proxy-go/api/shared"
"github.com/ElrondNetwork/elrond-proxy-go/data"
"github.com/gin-gonic/gin"
)

type actionsGroup struct {
facade ActionsFacadeHandler
*baseGroup
}

// NewActionsGroup returns a new instance of actionGroup
func NewActionsGroup(facadeHandler data.FacadeHandler) (*actionsGroup, error) {
facade, ok := facadeHandler.(ActionsFacadeHandler)
if !ok {
return nil, ErrWrongTypeAssertion
}

ng := &actionsGroup{
facade: facade,
baseGroup: &baseGroup{},
}

baseRoutesHandlers := map[string]*data.EndpointHandlerData{
"/reload-observers": {Handler: ng.updateObservers, Method: http.MethodPost},
"/reload-full-history-observers": {Handler: ng.updateFullHistoryObservers, Method: http.MethodPost},
}
ng.baseGroup.endpoints = baseRoutesHandlers

return ng, nil
}

func (group *actionsGroup) updateObservers(c *gin.Context) {
result := group.facade.ReloadObservers()
group.handleUpdateResponding(result, c)
}

func (group *actionsGroup) updateFullHistoryObservers(c *gin.Context) {
result := group.facade.ReloadFullHistoryObservers()
group.handleUpdateResponding(result, c)
}

func (group *actionsGroup) handleUpdateResponding(result data.NodesReloadResponse, c *gin.Context) {
if result.Error != "" {
httpCode := http.StatusInternalServerError
internalCode := data.ReturnCodeInternalError
if !result.OkRequest {
httpCode = http.StatusBadRequest
internalCode = data.ReturnCodeRequestError
}

shared.RespondWith(c, httpCode, result.Description, result.Error, internalCode)
return
}

shared.RespondWith(c, http.StatusOK, result.Description, "", data.ReturnCodeSuccess)
}
Loading

0 comments on commit 551567d

Please sign in to comment.