Skip to content

Commit

Permalink
- added sc calls executor
Browse files Browse the repository at this point in the history
  • Loading branch information
iulianpascalau committed Jul 3, 2024
1 parent b6a9b14 commit ffb5f68
Show file tree
Hide file tree
Showing 18 changed files with 1,085 additions and 47 deletions.
7 changes: 4 additions & 3 deletions clients/multiversx/mxClientDataGetter.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"sync"

"github.com/multiversx/mx-bridge-eth-go/clients"
"github.com/multiversx/mx-bridge-eth-go/errors"
"github.com/multiversx/mx-chain-core-go/core/check"
logger "github.com/multiversx/mx-chain-logger-go"
"github.com/multiversx/mx-sdk-go/builders"
Expand Down Expand Up @@ -111,7 +112,7 @@ func (dataGetter *mxClientDataGetter) ExecuteQueryReturningBytes(ctx context.Con
"response.ReturnCode", response.Data.ReturnCode,
"response.ReturnData", fmt.Sprintf("%+v", response.Data.ReturnData))
if response.Data.ReturnCode != okCodeAfterExecution {
return nil, NewQueryResponseError(
return nil, errors.NewQueryResponseError(
response.Data.ReturnCode,
response.Data.ReturnMessage,
request.FuncName,
Expand Down Expand Up @@ -178,7 +179,7 @@ func (dataGetter *mxClientDataGetter) parseBool(buff []byte, funcName string, ad

result, err := strconv.ParseBool(fmt.Sprintf("%d", buff[0]))
if err != nil {
return false, NewQueryResponseError(
return false, errors.NewQueryResponseError(
internalError,
fmt.Sprintf("error converting the received bytes to bool, %s", err.Error()),
funcName,
Expand Down Expand Up @@ -206,7 +207,7 @@ func (dataGetter *mxClientDataGetter) ExecuteQueryReturningUint64(ctx context.Co

num, err := parseUInt64FromByteSlice(response[0])
if err != nil {
return 0, NewQueryResponseError(
return 0, errors.NewQueryResponseError(
internalError,
err.Error(),
request.FuncName,
Expand Down
9 changes: 5 additions & 4 deletions clients/multiversx/mxClientDataGetter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"testing"

"github.com/multiversx/mx-bridge-eth-go/clients"
bridgeErrors "github.com/multiversx/mx-bridge-eth-go/errors"
"github.com/multiversx/mx-bridge-eth-go/parsers"
bridgeTests "github.com/multiversx/mx-bridge-eth-go/testsCommon/bridge"
"github.com/multiversx/mx-bridge-eth-go/testsCommon/interactors"
Expand Down Expand Up @@ -198,7 +199,7 @@ func TestMXClientDataGetter_ExecuteQueryReturningBytes(t *testing.T) {

dg, _ := NewMXClientDataGetter(args)

expectedErr := NewQueryResponseError(returnCode, returnMessage, calledFunction, getBech32Address(dg.multisigContractAddress), calledArgs...)
expectedErr := bridgeErrors.NewQueryResponseError(returnCode, returnMessage, calledFunction, getBech32Address(dg.multisigContractAddress), calledArgs...)
dg.proxy = &interactors.ProxyStub{
ExecuteVMQueryCalled: func(ctx context.Context, vmRequest *data.VmValueRequest) (*data.VmValuesResponseData, error) {
return &data.VmValuesResponseData{
Expand Down Expand Up @@ -306,7 +307,7 @@ func TestMXClientDataGetter_ExecuteQueryReturningBool(t *testing.T) {
dg, _ := NewMXClientDataGetter(args)
dg.proxy = createMockProxy([][]byte{[]byte("random bytes")})

expectedError := NewQueryResponseError(
expectedError := bridgeErrors.NewQueryResponseError(
internalError,
`error converting the received bytes to bool, strconv.ParseBool: parsing "114": invalid syntax`,
"",
Expand Down Expand Up @@ -374,7 +375,7 @@ func TestMXClientDataGetter_ExecuteQueryReturningUint64(t *testing.T) {
dg, _ := NewMXClientDataGetter(args)
dg.proxy = createMockProxy([][]byte{[]byte("random bytes")})

expectedError := NewQueryResponseError(
expectedError := bridgeErrors.NewQueryResponseError(
internalError,
errNotUint64Bytes.Error(),
"",
Expand Down Expand Up @@ -960,7 +961,7 @@ func TestMXClientDataGetter_GetTransactionsStatuses(t *testing.T) {

result, err := dg.GetTransactionsStatuses(context.Background(), batchID)
assert.Nil(t, result)
expectedErr := NewQueryResponseError(internalError, `error converting the received bytes to bool, strconv.ParseBool: parsing "56": invalid syntax`,
expectedErr := bridgeErrors.NewQueryResponseError(internalError, `error converting the received bytes to bool, strconv.ParseBool: parsing "56": invalid syntax`,
"getStatusesAfterExecution", "erd1qqqqqqqqqqqqqpgqzyuaqg3dl7rqlkudrsnm5ek0j3a97qevd8sszj0glf")
assert.Equal(t, expectedErr, err)
})
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package multiversx
package errors

import "fmt"

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package multiversx
package errors

import (
"testing"
Expand Down
14 changes: 14 additions & 0 deletions executors/multiversx/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package multiversx

import "errors"

var (
errInvalidNumberOfResponseLines = errors.New("invalid number of responses")
errNilProxy = errors.New("nil proxy")
errNilCodec = errors.New("nil codec")
errNilFilter = errors.New("nil filter")
errNilLogger = errors.New("nil logger")
errNilNonceTxHandler = errors.New("nil nonce transaction handler")
errNilPrivateKey = errors.New("nil private key")
errNilSingleSigner = errors.New("nil single signer")
)
12 changes: 11 additions & 1 deletion executors/interface.go → executors/multiversx/interface.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package executors
package multiversx

import (
"context"
Expand Down Expand Up @@ -30,10 +30,20 @@ type NonceTransactionsHandler interface {
ApplyNonceAndGasPrice(ctx context.Context, address core.AddressHandler, tx *transaction.FrontendTransaction) error
SendTransaction(ctx context.Context, tx *transaction.FrontendTransaction) (string, error)
Close() error
IsInterfaceNil() bool
}

// ScCallsExecuteFilter defines the operations supported by a filter that allows selective executions of batches
type ScCallsExecuteFilter interface {
ShouldExecute(callData parsers.ProxySCCompleteCallData) bool
IsInterfaceNil() bool
}

// Codec defines the operations implemented by a MultiversX codec
type Codec interface {
EncodeCallData(callData parsers.CallData) []byte
EncodeProxySCCompleteCallData(completeData parsers.ProxySCCompleteCallData) ([]byte, error)
DecodeCallData(buff []byte) (parsers.CallData, error)
DecodeProxySCCompleteCallData(buff []byte) (parsers.ProxySCCompleteCallData, error)
IsInterfaceNil() bool
}
266 changes: 266 additions & 0 deletions executors/multiversx/scCallsExecutor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,266 @@
package multiversx

import (
"context"
"encoding/hex"
"encoding/json"
"fmt"
"math/big"

"github.com/multiversx/mx-bridge-eth-go/errors"
"github.com/multiversx/mx-bridge-eth-go/parsers"
"github.com/multiversx/mx-chain-core-go/core/check"
"github.com/multiversx/mx-chain-core-go/data/transaction"
crypto "github.com/multiversx/mx-chain-crypto-go"
logger "github.com/multiversx/mx-chain-logger-go"
"github.com/multiversx/mx-sdk-go/builders"
"github.com/multiversx/mx-sdk-go/core"
"github.com/multiversx/mx-sdk-go/data"
)

const (
getPendingTransactionsFunction = "getPendingTransactions"
okCodeAfterExecution = "ok"
scProxyCallFunction = "executeWithAsnyc"
)

// ArgsScCallExecutor represents the DTO struct for creating a new instance of type scCallExecutor
type ArgsScCallExecutor struct {
ScProxyBech32Address string
Proxy Proxy
Codec Codec
Filter ScCallsExecuteFilter
Log logger.Logger
ExtraGasToExecute uint64
NonceTxHandler NonceTransactionsHandler
PrivateKey crypto.PrivateKey
SingleSigner crypto.SingleSigner
}

type scCallExecutor struct {
scProxyBech32Address string
proxy Proxy
codec Codec
filter ScCallsExecuteFilter
log logger.Logger
extraGasToExecute uint64
nonceTxHandler NonceTransactionsHandler
privateKey crypto.PrivateKey
singleSigner crypto.SingleSigner
senderAddress core.AddressHandler
}

// NewScCallExecutor creates a new instance of type scCallExecutor
func NewScCallExecutor(args ArgsScCallExecutor) (*scCallExecutor, error) {
err := checkArgs(args)
if err != nil {
return nil, err
}

publicKey := args.PrivateKey.GeneratePublic()
publicKeyBytes, err := publicKey.ToByteArray()
if err != nil {
return nil, err
}
senderAddress := data.NewAddressFromBytes(publicKeyBytes)

return &scCallExecutor{
scProxyBech32Address: args.ScProxyBech32Address,
proxy: args.Proxy,
codec: args.Codec,
filter: args.Filter,
log: args.Log,
extraGasToExecute: args.ExtraGasToExecute,
nonceTxHandler: args.NonceTxHandler,
privateKey: args.PrivateKey,
singleSigner: args.SingleSigner,
senderAddress: senderAddress,
}, nil
}

func checkArgs(args ArgsScCallExecutor) error {
if check.IfNil(args.Proxy) {
return errNilProxy
}
if check.IfNil(args.Codec) {
return errNilCodec
}
if check.IfNil(args.Filter) {
return errNilFilter
}
if check.IfNil(args.Log) {
return errNilLogger
}
if check.IfNil(args.NonceTxHandler) {
return errNilNonceTxHandler
}
if check.IfNil(args.PrivateKey) {
return errNilPrivateKey
}
if check.IfNil(args.SingleSigner) {
return errNilSingleSigner
}

return nil
}

// Execute will execute one step: get all pending operations, call the filter and send execution transactions
func (executor *scCallExecutor) Execute(ctx context.Context) error {
pendingOperations, err := executor.getPendingOperations(ctx)
if err != nil {
return err
}

filteredPendingOperations := executor.filterOperations(pendingOperations)

return executor.executeOperations(ctx, filteredPendingOperations)
}

func (executor *scCallExecutor) getPendingOperations(ctx context.Context) (map[uint64]parsers.ProxySCCompleteCallData, error) {
request := &data.VmValueRequest{
Address: executor.scProxyBech32Address,
FuncName: getPendingTransactionsFunction,
}

response, err := executor.proxy.ExecuteVMQuery(ctx, request)
if err != nil {
executor.log.Error("got error on VMQuery", "FuncName", request.FuncName,
"Args", request.Args, "SC address", request.Address, "Caller", request.CallerAddr, "error", err)
return nil, err
}
if response.Data.ReturnCode != okCodeAfterExecution {
return nil, errors.NewQueryResponseError(
response.Data.ReturnCode,
response.Data.ReturnMessage,
request.FuncName,
request.Address,
request.Args...,
)
}

return executor.parseResponse(response)
}

func (executor *scCallExecutor) parseResponse(response *data.VmValuesResponseData) (map[uint64]parsers.ProxySCCompleteCallData, error) {
numResponseLines := len(response.Data.ReturnData)
if numResponseLines%2 != 0 {
return nil, fmt.Errorf("%w: expected an even number, got %d", errInvalidNumberOfResponseLines, numResponseLines)
}

result := make(map[uint64]parsers.ProxySCCompleteCallData)

for i := 0; i < numResponseLines; i += 2 {
pendingOperationID := big.NewInt(0).SetBytes(response.Data.ReturnData[i])
callData, err := executor.codec.DecodeProxySCCompleteCallData(response.Data.ReturnData[i+1])
if err != nil {
return nil, fmt.Errorf("%w for ReturnData at index %d", err, i+1)
}

result[pendingOperationID.Uint64()] = callData
}

return result, nil
}

func (executor *scCallExecutor) filterOperations(pendingOperations map[uint64]parsers.ProxySCCompleteCallData) map[uint64]parsers.ProxySCCompleteCallData {
result := make(map[uint64]parsers.ProxySCCompleteCallData)
for id, callData := range pendingOperations {
if executor.filter.ShouldExecute(callData) {
result[id] = callData
}
}

return result
}

func (executor *scCallExecutor) executeOperations(ctx context.Context, pendingOperations map[uint64]parsers.ProxySCCompleteCallData) error {
networkConfig, err := executor.proxy.GetNetworkConfig(ctx)
if err != nil {
return fmt.Errorf("%w while fetching network configs", err)
}

for id, callData := range pendingOperations {
err = executor.executeOperation(id, callData, networkConfig)
if err != nil {
return fmt.Errorf("%w for call data: %s", err, callData)
}
}

return nil
}

func (executor *scCallExecutor) executeOperation(
id uint64,
callData parsers.ProxySCCompleteCallData,
networkConfig *data.NetworkConfig,
) error {
txBuilder := builders.NewTxDataBuilder()
txBuilder.Function(scProxyCallFunction).ArgInt64(int64(id))

dataBytes, err := txBuilder.ToDataBytes()
if err != nil {
return err
}

bech32Address, err := executor.senderAddress.AddressAsBech32String()
if err != nil {
return err
}

tx := &transaction.FrontendTransaction{
ChainID: networkConfig.ChainID,
Version: networkConfig.MinTransactionVersion,
GasLimit: callData.GasLimit + executor.extraGasToExecute,
Data: dataBytes,
Sender: bech32Address,
Receiver: executor.scProxyBech32Address,
Value: "0",
}

err = executor.nonceTxHandler.ApplyNonceAndGasPrice(context.Background(), executor.senderAddress, tx)
if err != nil {
return err
}

err = executor.signTransactionWithPrivateKey(tx)
if err != nil {
return err
}

hash, err := executor.nonceTxHandler.SendTransaction(context.Background(), tx)
if err != nil {
return err
}

executor.log.Info("sent transaction from executor",
"hash", hash,
"tx ID", id,
"call data", callData.String(),
"extra gas", executor.extraGasToExecute,
"sender", bech32Address)

return nil
}

// signTransactionWithPrivateKey signs a transaction with the client's private key
func (executor *scCallExecutor) signTransactionWithPrivateKey(tx *transaction.FrontendTransaction) error {
tx.Signature = ""
bytes, err := json.Marshal(&tx)
if err != nil {
return err
}

signature, err := executor.singleSigner.Sign(executor.privateKey, bytes)
if err != nil {
return err
}

tx.Signature = hex.EncodeToString(signature)

return nil
}

// IsInterfaceNil returns true if there is no value under the interface
func (executor *scCallExecutor) IsInterfaceNil() bool {
return executor == nil
}
Loading

0 comments on commit ffb5f68

Please sign in to comment.