Skip to content

Commit

Permalink
Merge pull request #6373 from multiversx/new-endpoint-scrs-by-tx-hash
Browse files Browse the repository at this point in the history
New endpoint /transaction/scrs-by-tx-hash/:txHash
  • Loading branch information
miiu96 authored Aug 14, 2024
2 parents 07626af + 3cbcc0c commit 4fa8c2d
Show file tree
Hide file tree
Showing 19 changed files with 342 additions and 5 deletions.
6 changes: 6 additions & 0 deletions api/errors/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ var ErrTxGenerationFailed = errors.New("transaction generation failed")
// ErrValidationEmptyTxHash signals that an empty tx hash was provided
var ErrValidationEmptyTxHash = errors.New("TxHash is empty")

// ErrValidationEmptySCRHash signals that provided smart contract result hash is empty
var ErrValidationEmptySCRHash = errors.New("SCRHash is empty")

// ErrInvalidBlockNonce signals that an invalid block nonce was provided
var ErrInvalidBlockNonce = errors.New("invalid block nonce")

Expand All @@ -79,6 +82,9 @@ var ErrValidationEmptyBlockHash = errors.New("block hash is empty")
// ErrGetTransaction signals an error happening when trying to fetch a transaction
var ErrGetTransaction = errors.New("getting transaction failed")

// ErrGetSmartContractResults signals an error happening when trying to fetch smart contract results
var ErrGetSmartContractResults = errors.New("getting smart contract results failed")

// ErrGetBlock signals an error happening when trying to fetch a block
var ErrGetBlock = errors.New("getting block failed")

Expand Down
66 changes: 66 additions & 0 deletions api/groups/transactionGroup.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,13 @@ const (
simulateTransactionEndpoint = "/transaction/simulate"
sendMultipleTransactionsEndpoint = "/transaction/send-multiple"
getTransactionEndpoint = "/transaction/:hash"
getScrsByTxHashEndpoint = "/transaction/scrs-by-tx-hash/:txhash"
sendTransactionPath = "/send"
simulateTransactionPath = "/simulate"
costPath = "/cost"
sendMultiplePath = "/send-multiple"
getTransactionPath = "/:txhash"
getScrsByTxHashPath = "/scrs-by-tx-hash/:txhash"
getTransactionsPool = "/pool"

queryParamWithResults = "withResults"
Expand All @@ -39,6 +41,7 @@ const (
queryParamFields = "fields"
queryParamLastNonce = "last-nonce"
queryParamNonceGaps = "nonce-gaps"
queryParameterScrHash = "scrHash"
)

// transactionFacadeHandler defines the methods to be implemented by a facade for transaction requests
Expand All @@ -49,6 +52,7 @@ type transactionFacadeHandler interface {
SendBulkTransactions([]*transaction.Transaction) (uint64, error)
SimulateTransactionExecution(tx *transaction.Transaction) (*txSimData.SimulationResultsWithVMOutput, error)
GetTransaction(hash string, withResults bool) (*transaction.ApiTransactionResult, error)
GetSCRsByTxHash(txHash string, scrHash string) ([]*transaction.ApiSmartContractResult, error)
GetTransactionsPool(fields string) (*common.TransactionsPoolAPIResponse, error)
GetTransactionsPoolForSender(sender, fields string) (*common.TransactionsPoolForSenderApiResponse, error)
GetLastPoolNonceForSender(sender string) (uint64, error)
Expand Down Expand Up @@ -137,6 +141,17 @@ func NewTransactionGroup(facade transactionFacadeHandler) (*transactionGroup, er
},
},
},
{
Path: getScrsByTxHashPath,
Method: http.MethodGet,
Handler: tg.getScrsByTxHash,
AdditionalMiddlewares: []shared.AdditionalMiddleware{
{
Middleware: middleware.CreateEndpointThrottlerFromFacade(getScrsByTxHashEndpoint, facade),
Position: shared.Before,
},
},
},
}
tg.endpoints = endpoints

Expand Down Expand Up @@ -421,6 +436,57 @@ func (tg *transactionGroup) sendMultipleTransactions(c *gin.Context) {
)
}

func (tg *transactionGroup) getScrsByTxHash(c *gin.Context) {
txhash := c.Param("txhash")
if txhash == "" {
c.JSON(
http.StatusBadRequest,
shared.GenericAPIResponse{
Data: nil,
Error: fmt.Sprintf("%s: %s", errors.ErrValidation.Error(), errors.ErrValidationEmptyTxHash.Error()),
Code: shared.ReturnCodeRequestError,
},
)
return
}
scrHashStr := c.Request.URL.Query().Get(queryParameterScrHash)
if scrHashStr == "" {
c.JSON(
http.StatusBadRequest,
shared.GenericAPIResponse{
Data: nil,
Error: fmt.Sprintf("%s: %s", errors.ErrValidation.Error(), errors.ErrValidationEmptySCRHash.Error()),
Code: shared.ReturnCodeRequestError,
},
)
return
}

start := time.Now()
scrs, err := tg.getFacade().GetSCRsByTxHash(txhash, scrHashStr)
if err != nil {
c.JSON(
http.StatusInternalServerError,
shared.GenericAPIResponse{
Data: nil,
Error: fmt.Sprintf("%s: %s", errors.ErrGetSmartContractResults.Error(), err.Error()),
Code: shared.ReturnCodeInternalError,
},
)
return
}
logging.LogAPIActionDurationIfNeeded(start, "API call: GetSCRsByTxHash")

c.JSON(
http.StatusOK,
shared.GenericAPIResponse{
Data: gin.H{"scrs": scrs},
Error: "",
Code: shared.ReturnCodeSuccess,
},
)
}

// getTransaction returns transaction details for a given txhash
func (tg *transactionGroup) getTransaction(c *gin.Context) {
txhash := c.Param("txhash")
Expand Down
71 changes: 71 additions & 0 deletions api/groups/transactionGroup_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,76 @@ func TestTransactionGroup_sendTransaction(t *testing.T) {
})
}

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

t.Run("get SCRsByTxHash empty scr hash should error", func(t *testing.T) {
facade := &mock.FacadeStub{}

transactionGroup, err := groups.NewTransactionGroup(facade)
require.NoError(t, err)

ws := startWebServer(transactionGroup, "transaction", getTransactionRoutesConfig())

req, _ := http.NewRequest(http.MethodGet, "/transaction/scrs-by-tx-hash/txHash", bytes.NewBuffer([]byte{}))
resp := httptest.NewRecorder()
ws.ServeHTTP(resp, req)

txResp := shared.GenericAPIResponse{}
loadResponse(resp.Body, &txResp)

assert.Equal(t, http.StatusBadRequest, resp.Code)
assert.True(t, strings.Contains(txResp.Error, apiErrors.ErrValidationEmptySCRHash.Error()))
assert.Empty(t, txResp.Data)
})
t.Run("get scrs facade error", func(t *testing.T) {
localErr := fmt.Errorf("error")
facade := &mock.FacadeStub{
GetSCRsByTxHashCalled: func(txHash string, scrHash string) ([]*dataTx.ApiSmartContractResult, error) {
return nil, localErr
},
}

transactionGroup, err := groups.NewTransactionGroup(facade)
require.NoError(t, err)

ws := startWebServer(transactionGroup, "transaction", getTransactionRoutesConfig())

req, _ := http.NewRequest(http.MethodGet, "/transaction/scrs-by-tx-hash/txhash?scrHash=hash", bytes.NewBuffer([]byte{}))
resp := httptest.NewRecorder()
ws.ServeHTTP(resp, req)

txResp := shared.GenericAPIResponse{}
loadResponse(resp.Body, &txResp)

assert.Equal(t, http.StatusInternalServerError, resp.Code)
assert.True(t, strings.Contains(txResp.Error, localErr.Error()))
assert.Empty(t, txResp.Data)
})
t.Run("get scrs should work", func(t *testing.T) {
facade := &mock.FacadeStub{
GetSCRsByTxHashCalled: func(txHash string, scrHash string) ([]*dataTx.ApiSmartContractResult, error) {
return []*dataTx.ApiSmartContractResult{}, nil
},
}

transactionGroup, err := groups.NewTransactionGroup(facade)
require.NoError(t, err)

ws := startWebServer(transactionGroup, "transaction", getTransactionRoutesConfig())

req, _ := http.NewRequest(http.MethodGet, "/transaction/scrs-by-tx-hash/txhash?scrHash=hash", bytes.NewBuffer([]byte{}))
resp := httptest.NewRecorder()
ws.ServeHTTP(resp, req)

txResp := shared.GenericAPIResponse{}
loadResponse(resp.Body, &txResp)

assert.Equal(t, http.StatusOK, resp.Code)
assert.Equal(t, "", txResp.Error)
})
}

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

Expand Down Expand Up @@ -1125,6 +1195,7 @@ func getTransactionRoutesConfig() config.ApiRoutesConfig {
{Name: "/:txhash", Open: true},
{Name: "/:txhash/status", Open: true},
{Name: "/simulate", Open: true},
{Name: "/scrs-by-tx-hash/:txhash", Open: true},
},
},
},
Expand Down
10 changes: 10 additions & 0 deletions api/mock/facadeStub.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,16 @@ type FacadeStub struct {
GetWaitingEpochsLeftForPublicKeyCalled func(publicKey string) (uint32, error)
P2PPrometheusMetricsEnabledCalled func() bool
AuctionListHandler func() ([]*common.AuctionListValidatorAPIResponse, error)
GetSCRsByTxHashCalled func(txHash string, scrHash string) ([]*transaction.ApiSmartContractResult, error)
}

// GetSCRsByTxHash -
func (f *FacadeStub) GetSCRsByTxHash(txHash string, scrHash string) ([]*transaction.ApiSmartContractResult, error) {
if f.GetSCRsByTxHashCalled != nil {
return f.GetSCRsByTxHashCalled(txHash, scrHash)
}

return nil, nil
}

// GetTokenSupply -
Expand Down
1 change: 1 addition & 0 deletions api/shared/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ type FacadeHandler interface {
GetEligibleManagedKeys() ([]string, error)
GetWaitingManagedKeys() ([]string, error)
GetWaitingEpochsLeftForPublicKey(publicKey string) (uint32, error)
GetSCRsByTxHash(txHash string, scrHash string) ([]*transaction.ApiSmartContractResult, error)
P2PPrometheusMetricsEnabled() bool
IsInterfaceNil() bool
}
3 changes: 3 additions & 0 deletions cmd/node/config/api.toml
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,9 @@

# /transaction/:txhash will return the transaction in JSON format based on its hash
{ Name = "/:txhash", Open = true },

# /transaction/scrs-by-tx-hash/:txhash will return the smart contract results generated by the provided transaction hash
{ Name = "/scrs-by-tx-hash/:txhash", Open = true },
]

[APIPackages.block]
Expand Down
5 changes: 5 additions & 0 deletions facade/initial/initialNodeFacade.go
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,11 @@ func (inf *initialNodeFacade) IsDataTrieMigrated(_ string, _ api.AccountQueryOpt
return false, errNodeStarting
}

// GetSCRsByTxHash return a nil slice and error
func (inf *initialNodeFacade) GetSCRsByTxHash(_ string, _ string) ([]*transaction.ApiSmartContractResult, error) {
return nil, errNodeStarting
}

// GetManagedKeysCount returns 0
func (inf *initialNodeFacade) GetManagedKeysCount() int {
return 0
Expand Down
1 change: 1 addition & 0 deletions facade/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ type ApiResolver interface {
GetDirectStakedList(ctx context.Context) ([]*api.DirectStakedValue, error)
GetDelegatorsList(ctx context.Context) ([]*api.Delegator, error)
GetTransaction(hash string, withResults bool) (*transaction.ApiTransactionResult, error)
GetSCRsByTxHash(txHash string, scrHash string) ([]*transaction.ApiSmartContractResult, error)
GetTransactionsPool(fields string) (*common.TransactionsPoolAPIResponse, error)
GetTransactionsPoolForSender(sender, fields string) (*common.TransactionsPoolForSenderApiResponse, error)
GetLastPoolNonceForSender(sender string) (uint64, error)
Expand Down
10 changes: 10 additions & 0 deletions facade/mock/apiResolverStub.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,16 @@ type ApiResolverStub struct {
GetEligibleManagedKeysCalled func() ([]string, error)
GetWaitingManagedKeysCalled func() ([]string, error)
GetWaitingEpochsLeftForPublicKeyCalled func(publicKey string) (uint32, error)
GetSCRsByTxHashCalled func(txHash string, scrHash string) ([]*transaction.ApiSmartContractResult, error)
}

// GetSCRsByTxHash -
func (ars *ApiResolverStub) GetSCRsByTxHash(txHash string, scrHash string) ([]*transaction.ApiSmartContractResult, error) {
if ars.GetSCRsByTxHashCalled != nil {
return ars.GetSCRsByTxHashCalled(txHash, scrHash)
}

return nil, nil
}

// GetTransaction -
Expand Down
5 changes: 5 additions & 0 deletions facade/nodeFacade.go
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,11 @@ func (nf *nodeFacade) GetTransaction(hash string, withResults bool) (*transactio
return nf.apiResolver.GetTransaction(hash, withResults)
}

// GetSCRsByTxHash will return a list of smart contract results based on a provided tx hash and smart contract result hash
func (nf *nodeFacade) GetSCRsByTxHash(txHash string, scrHash string) ([]*transaction.ApiSmartContractResult, error) {
return nf.apiResolver.GetSCRsByTxHash(txHash, scrHash)
}

// GetTransactionsPool will return a structure containing the transactions pool that is to be returned on API calls
func (nf *nodeFacade) GetTransactionsPool(fields string) (*common.TransactionsPoolAPIResponse, error) {
return nf.apiResolver.GetTransactionsPool(fields)
Expand Down
5 changes: 5 additions & 0 deletions integrationTests/chainSimulator/vm/esdtTokens_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,11 @@ func TestChainSimulator_Api_TokenType(t *testing.T) {
require.NotNil(t, txResult)
require.Equal(t, "success", txResult.Status.String())

scrs, err := cs.GetNodeHandler(core.MetachainShardId).GetFacadeHandler().GetSCRsByTxHash(txResult.Hash, txResult.SmartContractResults[0].Hash)
require.Nil(t, err)
require.NotNil(t, scrs)
require.Equal(t, len(txResult.SmartContractResults), len(scrs))

nftTokenID := txResult.Logs.Events[0].Topics[0]
setAddressEsdtRoles(t, cs, nonce, addrs[0], nftTokenID, roles)
nonce++
Expand Down
1 change: 1 addition & 0 deletions integrationTests/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,5 +118,6 @@ type Facade interface {
GetEligibleManagedKeys() ([]string, error)
GetWaitingManagedKeys() ([]string, error)
GetWaitingEpochsLeftForPublicKey(publicKey string) (uint32, error)
GetSCRsByTxHash(txHash string, scrHash string) ([]*transaction.ApiSmartContractResult, error)
IsInterfaceNil() bool
}
1 change: 1 addition & 0 deletions node/external/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ type DelegatedListHandler interface {
// APITransactionHandler defines what an API transaction handler should be able to do
type APITransactionHandler interface {
GetTransaction(txHash string, withResults bool) (*transaction.ApiTransactionResult, error)
GetSCRsByTxHash(txHash string, scrHash string) ([]*transaction.ApiSmartContractResult, error)
GetTransactionsPool(fields string) (*common.TransactionsPoolAPIResponse, error)
GetTransactionsPoolForSender(sender, fields string) (*common.TransactionsPoolForSenderApiResponse, error)
GetLastPoolNonceForSender(sender string) (uint64, error)
Expand Down
5 changes: 5 additions & 0 deletions node/external/nodeApiResolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,11 @@ func (nar *nodeApiResolver) GetTransaction(hash string, withResults bool) (*tran
return nar.apiTransactionHandler.GetTransaction(hash, withResults)
}

// GetSCRsByTxHash will return a list of smart contract results based on a provided tx hash and smart contract result hash
func (nar *nodeApiResolver) GetSCRsByTxHash(txHash string, scrHash string) ([]*transaction.ApiSmartContractResult, error) {
return nar.apiTransactionHandler.GetSCRsByTxHash(txHash, scrHash)
}

// GetTransactionsPool will return a structure containing the transactions pool that is to be returned on API calls
func (nar *nodeApiResolver) GetTransactionsPool(fields string) (*common.TransactionsPoolAPIResponse, error) {
return nar.apiTransactionHandler.GetTransactionsPool(fields)
Expand Down
44 changes: 44 additions & 0 deletions node/external/transactionAPI/apiTransactionProcessor.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package transactionAPI

import (
"encoding/hex"
"errors"
"fmt"
"sort"
"strings"
Expand Down Expand Up @@ -86,6 +87,49 @@ func NewAPITransactionProcessor(args *ArgAPITransactionProcessor) (*apiTransacti
}, nil
}

// GetSCRsByTxHash will return a list of smart contract results based on a provided tx hash and smart contract result hash
func (atp *apiTransactionProcessor) GetSCRsByTxHash(txHash string, scrHash string) ([]*transaction.ApiSmartContractResult, error) {
decodedScrHash, err := hex.DecodeString(scrHash)
if err != nil {
return nil, err
}

decodedTxHash, err := hex.DecodeString(txHash)
if err != nil {
return nil, err
}

if !atp.historyRepository.IsEnabled() {
return nil, fmt.Errorf("cannot return smat contract results: %w", ErrDBLookExtensionIsNotEnabled)
}

miniblockMetadata, err := atp.historyRepository.GetMiniblockMetadataByTxHash(decodedScrHash)
if err != nil {
return nil, fmt.Errorf("%s: %w", ErrTransactionNotFound.Error(), err)
}

resultsHashes, err := atp.historyRepository.GetResultsHashesByTxHash(decodedTxHash, miniblockMetadata.Epoch)
if err != nil {
// It's perfectly normal to have transactions without SCRs.
if errors.Is(err, dblookupext.ErrNotFoundInStorage) {
return []*transaction.ApiSmartContractResult{}, nil
}
return nil, err
}

scrsAPI := make([]*transaction.ApiSmartContractResult, 0, len(resultsHashes.ScResultsHashesAndEpoch))
for _, scrHashesEpoch := range resultsHashes.ScResultsHashesAndEpoch {
scrs, errGet := atp.transactionResultsProcessor.getSmartContractResultsInTransactionByHashesAndEpoch(scrHashesEpoch.ScResultsHashes, scrHashesEpoch.Epoch)
if errGet != nil {
return nil, errGet
}

scrsAPI = append(scrsAPI, scrs...)
}

return scrsAPI, nil
}

// GetTransaction gets the transaction based on the given hash. It will search in the cache and the storage and
// will return the transaction in a format which can be respected by all types of transactions (normal, reward or unsigned)
func (atp *apiTransactionProcessor) GetTransaction(txHash string, withResults bool) (*transaction.ApiTransactionResult, error) {
Expand Down
Loading

0 comments on commit 4fa8c2d

Please sign in to comment.