diff --git a/app/clients/evm/client_pool.go b/app/clients/evm/client_pool.go new file mode 100644 index 000000000..7ae28ee9b --- /dev/null +++ b/app/clients/evm/client_pool.go @@ -0,0 +1,343 @@ +/* + * Copyright 2022 LimeChain Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package evm + +import ( + "context" + "math/big" + + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/limechain/hedera-eth-bridge-validator/app/domain/client" + "github.com/limechain/hedera-eth-bridge-validator/config" +) + +type ClientPool struct { + clients []client.EVM + retries int +} + +func NewClientPool(c config.EvmPool, chainId uint64) *ClientPool { + nodeURLs := c.NodeUrls + clients := make([]client.EVM, 0, len(nodeURLs)) + for _, nodeURL := range nodeURLs { + configEvm := config.Evm{ + BlockConfirmations: c.BlockConfirmations, + NodeUrl: nodeURL, + PrivateKey: c.PrivateKey, + StartBlock: c.StartBlock, + PollingInterval: c.PollingInterval, + MaxLogsBlocks: c.MaxLogsBlocks, + } + clients = append(clients, NewClient(configEvm, chainId)) + } + + retry := len(clients) * 3 + + return &ClientPool{ + clients: clients, + retries: retry, + } +} + +func (cp *ClientPool) getClient(idx int) client.EVM { + return cp.clients[idx%len(cp.clients)] +} + +func (cp *ClientPool) retryOperation(operation func(client.EVM) (interface{}, error)) (interface{}, error) { + var err error + for i := 0; i < cp.retries; i++ { + client := cp.getClient(i) + result, e := operation(client) + if e == nil { + return result, nil + } + err = e + } + + return nil, err +} + +func (cp *ClientPool) ChainID(ctx context.Context) (*big.Int, error) { + operation := func(c client.EVM) (interface{}, error) { + return c.ChainID(ctx) + } + + result, err := cp.retryOperation(operation) + if err != nil { + return nil, err + } + + return result.(*big.Int), nil +} + +func (cp *ClientPool) CodeAt(ctx context.Context, contract common.Address, blockNumber *big.Int) ([]byte, error) { + operation := func(c client.EVM) (interface{}, error) { + return c.CodeAt(ctx, contract, blockNumber) + } + + result, err := cp.retryOperation(operation) + if err != nil { + return nil, err + } + + return result.([]byte), nil +} + +func (cp *ClientPool) CallContract(ctx context.Context, call ethereum.CallMsg, blockNumber *big.Int) ([]byte, error) { + operation := func(c client.EVM) (interface{}, error) { + return c.CallContract(ctx, call, blockNumber) + } + + result, err := cp.retryOperation(operation) + if err != nil { + return nil, err + } + + return result.([]byte), nil +} + +func (cp *ClientPool) HeaderByNumber(ctx context.Context, number *big.Int) (*types.Header, error) { + operation := func(c client.EVM) (interface{}, error) { + return c.HeaderByNumber(ctx, number) + } + + result, err := cp.retryOperation(operation) + if err != nil { + return nil, err + } + + return result.(*types.Header), nil +} + +func (cp *ClientPool) PendingCodeAt(ctx context.Context, account common.Address) ([]byte, error) { + operation := func(c client.EVM) (interface{}, error) { + return c.PendingCodeAt(ctx, account) + } + + result, err := cp.retryOperation(operation) + if err != nil { + return nil, err + } + + return result.([]byte), nil +} + +func (cp *ClientPool) PendingNonceAt(ctx context.Context, account common.Address) (uint64, error) { + operation := func(c client.EVM) (interface{}, error) { + return c.PendingNonceAt(ctx, account) + } + + result, err := cp.retryOperation(operation) + if err != nil { + return 0, err + } + + return result.(uint64), nil +} + +func (cp *ClientPool) SuggestGasPrice(ctx context.Context) (*big.Int, error) { + operation := func(c client.EVM) (interface{}, error) { + return c.SuggestGasPrice(ctx) + } + + result, err := cp.retryOperation(operation) + if err != nil { + return nil, err + } + + return result.(*big.Int), nil +} + +func (cp *ClientPool) SuggestGasTipCap(ctx context.Context) (*big.Int, error) { + operation := func(c client.EVM) (interface{}, error) { + return c.SuggestGasTipCap(ctx) + } + + result, err := cp.retryOperation(operation) + if err != nil { + return nil, err + } + + return result.(*big.Int), nil +} + +func (cp *ClientPool) EstimateGas(ctx context.Context, call ethereum.CallMsg) (gas uint64, err error) { + operation := func(c client.EVM) (interface{}, error) { + return c.EstimateGas(ctx, call) + } + + result, err := cp.retryOperation(operation) + if err != nil { + return 0, err + } + + return result.(uint64), nil +} + +func (cp *ClientPool) SendTransaction(ctx context.Context, tx *types.Transaction) error { + operation := func(c client.EVM) (interface{}, error) { + return nil, c.SendTransaction(ctx, tx) + } + + _, err := cp.retryOperation(operation) + return err +} + +func (cp *ClientPool) FilterLogs(ctx context.Context, query ethereum.FilterQuery) ([]types.Log, error) { + operation := func(c client.EVM) (interface{}, error) { + return c.FilterLogs(ctx, query) + } + + result, err := cp.retryOperation(operation) + if err != nil { + return nil, err + } + + return result.([]types.Log), nil +} + +func (cp *ClientPool) SubscribeFilterLogs(ctx context.Context, query ethereum.FilterQuery, ch chan<- types.Log) (ethereum.Subscription, error) { + operation := func(c client.EVM) (interface{}, error) { + return c.SubscribeFilterLogs(ctx, query, ch) + } + + result, err := cp.retryOperation(operation) + if err != nil { + return nil, err + } + + return result.(ethereum.Subscription), nil +} + +func (cp *ClientPool) BlockNumber(ctx context.Context) (uint64, error) { + operation := func(c client.EVM) (interface{}, error) { + return c.BlockNumber(ctx) + } + + result, err := cp.retryOperation(operation) + if err != nil { + return 0, err + } + + return result.(uint64), nil +} + +func (cp *ClientPool) ValidateContractDeployedAt(contractAddress string) (*common.Address, error) { + operation := func(c client.EVM) (interface{}, error) { + return c.ValidateContractDeployedAt(contractAddress) + } + + result, err := cp.retryOperation(operation) + if err != nil { + return nil, err + } + + return result.(*common.Address), nil +} + +func (cp *ClientPool) RetryBlockNumber() (uint64, error) { + operation := func(c client.EVM) (interface{}, error) { + return c.RetryBlockNumber() + } + + result, err := cp.retryOperation(operation) + if err != nil { + return 0, err + } + + return result.(uint64), nil +} + +func (cp *ClientPool) RetryFilterLogs(query ethereum.FilterQuery) ([]types.Log, error) { + operation := func(c client.EVM) (interface{}, error) { + return c.RetryFilterLogs(query) + } + + result, err := cp.retryOperation(operation) + if err != nil { + return nil, err + } + + return result.([]types.Log), nil +} + +func (cp *ClientPool) WaitForTransactionReceipt(hash common.Hash) (*types.Receipt, error) { + operation := func(c client.EVM) (interface{}, error) { + return c.WaitForTransactionReceipt(hash) + } + + result, err := cp.retryOperation(operation) + if err != nil { + return nil, err + } + + return result.(*types.Receipt), nil +} + +func (cp *ClientPool) RetryTransactionByHash(hash common.Hash) (*types.Transaction, error) { + operation := func(c client.EVM) (interface{}, error) { + return c.RetryTransactionByHash(hash) + } + + result, err := cp.retryOperation(operation) + if err != nil { + return nil, err + } + + return result.(*types.Transaction), nil +} + +func (cp *ClientPool) WaitForTransactionCallback(hex string, onSuccess, onRevert func(), onError func(err error)) { + cp.clients[0].WaitForTransactionCallback(hex, onSuccess, onRevert, onError) +} + +func (cp *ClientPool) WaitForConfirmations(raw types.Log) error { + operation := func(c client.EVM) (interface{}, error) { + return nil, c.WaitForConfirmations(raw) + } + + _, err := cp.retryOperation(operation) + return err +} + +func (cp *ClientPool) GetChainID() uint64 { + return cp.clients[0].GetChainID() +} + +func (cp *ClientPool) SetChainID(chainID uint64) { + for _, client := range cp.clients { + client.SetChainID(chainID) + } +} + +func (cp *ClientPool) GetClient() client.Core { + return cp.clients[0].GetClient() +} + +func (cp *ClientPool) GetPrivateKey() string { + return cp.clients[0].GetPrivateKey() +} + +func (cp *ClientPool) BlockConfirmations() uint64 { + return cp.clients[0].BlockConfirmations() +} + +func (cp *ClientPool) GetBlockTimestamp(blockNumber *big.Int) uint64 { + return cp.clients[0].GetBlockTimestamp(blockNumber) +} diff --git a/app/clients/evm/client_pool_test.go b/app/clients/evm/client_pool_test.go new file mode 100644 index 000000000..00cdc1286 --- /dev/null +++ b/app/clients/evm/client_pool_test.go @@ -0,0 +1,447 @@ +/* + * Copyright 2022 LimeChain Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package evm + +import ( + "context" + "github.com/ethereum/go-ethereum/common" + "github.com/limechain/hedera-eth-bridge-validator/test/mocks" + + "github.com/limechain/hedera-eth-bridge-validator/app/domain/client" + "github.com/stretchr/testify/assert" + "math/big" + "testing" + + "github.com/limechain/hedera-eth-bridge-validator/config" + + "errors" + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/core/types" + + "github.com/stretchr/testify/mock" + + "time" +) + +var ( + cp *ClientPool + retries = 3 +) + +func setupCP() { + setup() + evmList := make([]client.EVM, 0) + evmList = append(evmList, c) + + cp = &ClientPool{ + clients: evmList, + retries: retries, + } +} + +func TestNewClientPool(t *testing.T) { + nodeUrls := []string{"http://localhost:8545", "http://localhost:8546"} + configEvmPool := config.EvmPool{ + BlockConfirmations: 3, + NodeUrls: nodeUrls, + PrivateKey: "0x000000000", + StartBlock: 88, + PollingInterval: 5, + MaxLogsBlocks: 10, + } + + configEvm := config.Evm{ + BlockConfirmations: configEvmPool.BlockConfirmations, + NodeUrl: configEvmPool.NodeUrls[0], + PrivateKey: configEvmPool.PrivateKey, + StartBlock: configEvmPool.StartBlock, + PollingInterval: configEvmPool.PollingInterval, + MaxLogsBlocks: configEvmPool.MaxLogsBlocks, + } + + client := NewClient(configEvm, 256) + clientPool := NewClientPool(configEvmPool, 256) + assert.Equal(t, 6, clientPool.retries) + + assert.Equal(t, client.GetChainID(), clientPool.GetChainID()) + assert.Equal(t, client.GetPrivateKey(), clientPool.GetPrivateKey()) +} + +func TestClientPool_SetChainID(t *testing.T) { + setupCP() + cp.SetChainID(chainId) + assert.Equal(t, chainId, cp.GetChainID()) +} + +func TestClientPool_GetChainID(t *testing.T) { + setupCP() + cp.SetChainID(chainId) + assert.Equal(t, chainId, cp.GetChainID()) +} + +func TestClientPool_ChainID(t *testing.T) { + setupCP() + mocks.MEVMCoreClient.On("ChainID", context.Background()).Return(big.NewInt(1), nil) + chain, err := cp.ChainID(context.Background()) + if err != nil { + t.Fatal(err) + } + assert.Equal(t, big.NewInt(1), chain) +} + +func TestClientPool_ValidateContractDeployedAt(t *testing.T) { + setupCP() + + var nilBlockNumber *big.Int = nil + mocks.MEVMCoreClient.On("CodeAt", context.Background(), common.HexToAddress(address), nilBlockNumber).Return([]byte{0x1}, nil) + + _, err := cp.ValidateContractDeployedAt(address) + if err != nil { + t.Fatal(err) + } +} + +func TestClientPool_ValidateContractDeployedAt_CodeAtFails(t *testing.T) { + setupCP() + + var nilBlockNumber *big.Int = nil + mocks.MEVMCoreClient.On("CodeAt", context.Background(), common.HexToAddress(address), nilBlockNumber).Return(nil, errors.New("some-error")) + + result, err := cp.ValidateContractDeployedAt(address) + assert.NotNil(t, err) + assert.Nil(t, result) +} + +func TestClientPool_ValidateContractDeployedAt_NotASmartContract(t *testing.T) { + setupCP() + + var nilBlockNumber *big.Int = nil + mocks.MEVMCoreClient.On("CodeAt", context.Background(), common.HexToAddress(address), nilBlockNumber).Return([]byte{}, nil) + + result, err := cp.ValidateContractDeployedAt(address) + assert.NotNil(t, err) + assert.Nil(t, result) +} + +func TestClientPool_GetClient(t *testing.T) { + setupCP() + // assert.Equal(t, cp.Core, cp.GetClient()) + assert.Equal(t, c.Core, cp.GetClient()) +} + +func TestClientPool_GetBlockTimestamp(t *testing.T) { + setupCP() + now := uint64(time.Now().Unix()) + blockNumber := big.NewInt(1) + mocks.MEVMCoreClient.On("HeaderByNumber", context.Background(), blockNumber).Return(&types.Header{Time: now}, nil) + ts := cp.GetBlockTimestamp(blockNumber) + assert.Equal(t, now, ts) +} + +func TestClientPool_GetBlockTimestamp_Fails(t *testing.T) { + setupCP() + blockNumber := big.NewInt(1) + now := uint64(time.Now().Unix()) + mocks.MEVMCoreClient.On("HeaderByNumber", context.Background(), blockNumber).Return(nil, errors.New("some-error")).Once() + mocks.MEVMCoreClient.On("HeaderByNumber", context.Background(), blockNumber).Return(&types.Header{Time: now}, nil) + res := cp.GetBlockTimestamp(blockNumber) + assert.Equal(t, now, res) +} + +func TestClientPool_WaitForTransactionReceipt_NotFound(t *testing.T) { + setupCP() + + hash := common.HexToHash(address) + mocks.MEVMCoreClient.On("TransactionByHash", context.Background(), hash).Return(nil, false, ethereum.NotFound) + + receipt, err := cp.WaitForTransactionReceipt(hash) + assert.Error(t, ethereum.NotFound, err) + assert.Nil(t, receipt) +} + +func TestClientPool_GetPrivateKey(t *testing.T) { + setupCP() + assert.Equal(t, c.config.PrivateKey, cp.GetPrivateKey()) +} + +func TestClientPool_WaitForConfirmations(t *testing.T) { + setupCP() + + log := types.Log{ + BlockNumber: 20, + } + + mocks.MEVMCoreClient.On("BlockNumber", context.Background()).Return(uint64(20), nil) + mocks.MEVMCoreClient.On("TransactionReceipt", context.Background(), log.TxHash).Return(&types.Receipt{ + BlockNumber: big.NewInt(20), + }, nil) + + err := cp.WaitForConfirmations(log) + assert.Nil(t, err) +} + +func TestClientPool_WaitForConfirmations_MovedFromOriginalBlock(t *testing.T) { + setupCP() + + log := types.Log{ + BlockNumber: 19, + } + + mocks.MEVMCoreClient.On("BlockNumber", context.Background()).Return(uint64(20), nil) + mocks.MEVMCoreClient.On("TransactionReceipt", context.Background(), log.TxHash).Return(&types.Receipt{ + BlockNumber: big.NewInt(20), + }, nil) + + err := cp.WaitForConfirmations(log) + assert.Error(t, errors.New("moved from original block"), err) +} + +func TestClientPool_WaitForConfirmations_TransactionReceipt_EthereumNotFound(t *testing.T) { + setupCP() + + log := types.Log{} + + mocks.MEVMCoreClient.On("BlockNumber", context.Background()).Return(uint64(20), nil) + mocks.MEVMCoreClient.On("TransactionReceipt", context.Background(), log.TxHash).Return(&types.Receipt{}, ethereum.NotFound) + + err := cp.WaitForConfirmations(log) + assert.Error(t, ethereum.NotFound, err) +} + +func TestClientPool_WaitForConfirmations_TransactionReceipt_OtherError(t *testing.T) { + setupCP() + + log := types.Log{} + + mocks.MEVMCoreClient.On("BlockNumber", context.Background()).Return(uint64(20), nil) + mocks.MEVMCoreClient.On("TransactionReceipt", context.Background(), log.TxHash).Return(&types.Receipt{}, errors.New("some-error")) + + err := cp.WaitForConfirmations(log) + assert.Error(t, errors.New("some-error"), err) +} + +func TestClientPool_WaitForConfirmations_BlockNumberFails(t *testing.T) { + setupCP() + + mocks.MEVMCoreClient.On("BlockNumber", context.Background()).Return(uint64(0), errors.New("some-error")) + + err := cp.WaitForConfirmations(types.Log{}) + assert.NotNil(t, err) + mocks.MEVMCoreClient.AssertNotCalled(t, "TransactionReceipt", context.Background(), mock.Anything) +} + +func TestClientPool_CodeAt(t *testing.T) { + setupCP() + ctx := context.TODO() + contractAddress := common.HexToAddress(address) + blockNumber := big.NewInt(1) + code := []byte{1, 2, 3, 4} + mocks.MEVMCoreClient.On("CodeAt", ctx, contractAddress, blockNumber).Return([]byte(nil), errors.New("error")).Once(). + On("CodeAt", ctx, contractAddress, blockNumber).Return([]byte(nil), errors.New("error")).Once(). + On("CodeAt", ctx, contractAddress, blockNumber).Return(code, nil).Once() + + res, err := cp.CodeAt(ctx, contractAddress, blockNumber) + + assert.NoError(t, err) + assert.Equal(t, code, res) + mocks.MEVMCoreClient.AssertNumberOfCalls(t, "CodeAt", 3) +} + +func TestClientPool_HeaderByNumber(t *testing.T) { + setupCP() + ctx := context.TODO() + number := big.NewInt(1) + header := &types.Header{} + mocks.MEVMCoreClient.On("HeaderByNumber", ctx, number).Return((*types.Header)(nil), errors.New("error")).Once(). + On("HeaderByNumber", ctx, number).Return((*types.Header)(nil), errors.New("error")).Once(). + On("HeaderByNumber", ctx, number).Return(header, nil).Once() + + res, err := cp.HeaderByNumber(ctx, number) + + assert.NoError(t, err) + assert.Equal(t, header, res) + mocks.MEVMCoreClient.AssertNumberOfCalls(t, "HeaderByNumber", 3) +} + +func TestClientPool_SuggestGasPrice(t *testing.T) { + setupCP() + ctx := context.TODO() + price := big.NewInt(1) + mocks.MEVMCoreClient.On("SuggestGasPrice", ctx).Return(nil, errors.New("error")).Once(). + On("SuggestGasPrice", ctx).Return(nil, errors.New("error")).Once(). + On("SuggestGasPrice", ctx).Return(price, nil).Once() + + res, err := cp.SuggestGasPrice(ctx) + + assert.NoError(t, err) + assert.Equal(t, price, res) + mocks.MEVMCoreClient.AssertNumberOfCalls(t, "SuggestGasPrice", 3) +} + +func TestClientPool_SuggestGasTipCap(t *testing.T) { + setupCP() + ctx := context.TODO() + cap := big.NewInt(1) + mocks.MEVMCoreClient.On("SuggestGasTipCap", ctx).Return(nil, errors.New("error")).Once(). + On("SuggestGasTipCap", ctx).Return(nil, errors.New("error")).Once(). + On("SuggestGasTipCap", ctx).Return(cap, nil).Once() + + res, err := cp.SuggestGasTipCap(ctx) + + assert.NoError(t, err) + assert.Equal(t, cap, res) + mocks.MEVMCoreClient.AssertNumberOfCalls(t, "SuggestGasTipCap", 3) +} + +func TestClientPool_EstimateGas(t *testing.T) { + setupCP() + ctx := context.TODO() + call := ethereum.CallMsg{} + gas := uint64(1) + mocks.MEVMCoreClient.On("EstimateGas", ctx, call).Return(uint64(0), errors.New("error")).Once(). + On("EstimateGas", ctx, call).Return(uint64(0), errors.New("error")).Once(). + On("EstimateGas", ctx, call).Return(gas, nil).Once() + + res, err := cp.EstimateGas(ctx, call) + + assert.NoError(t, err) + assert.Equal(t, gas, res) + mocks.MEVMCoreClient.AssertNumberOfCalls(t, "EstimateGas", 3) +} + +func TestClientPool_SendTransaction(t *testing.T) { + setupCP() + ctx := context.TODO() + tx := &types.Transaction{} + mocks.MEVMCoreClient.On("SendTransaction", ctx, tx).Return(errors.New("error")).Once(). + On("SendTransaction", ctx, tx).Return(errors.New("error")).Once(). + On("SendTransaction", ctx, tx).Return(nil).Once() + + err := cp.SendTransaction(ctx, tx) + + assert.NoError(t, err) + mocks.MEVMCoreClient.AssertNumberOfCalls(t, "SendTransaction", 3) +} + +func TestClientPool_FilterLogs(t *testing.T) { + setupCP() + ctx := context.TODO() + query := ethereum.FilterQuery{} + logs := []types.Log{{}} + mocks.MEVMCoreClient.On("FilterLogs", ctx, query).Return(nil, errors.New("error")).Once(). + On("FilterLogs", ctx, query).Return(nil, errors.New("error")).Once(). + On("FilterLogs", ctx, query).Return(logs, nil).Once() + + res, err := cp.FilterLogs(ctx, query) + + assert.NoError(t, err) + assert.Equal(t, logs, res) + mocks.MEVMCoreClient.AssertNumberOfCalls(t, "FilterLogs", 3) +} + +func TestClientPool_SubscribeFilterLogs(t *testing.T) { + setupCP() + ctx := context.TODO() + query := ethereum.FilterQuery{} + ch := make(chan<- types.Log) + sub := new(MockSubscription) + mocks.MEVMCoreClient.On("SubscribeFilterLogs", ctx, query, ch).Return(nil, errors.New("error")).Once(). + On("SubscribeFilterLogs", ctx, query, ch).Return(nil, errors.New("error")).Once(). + On("SubscribeFilterLogs", ctx, query, ch).Return(sub, nil).Once() + + res, err := cp.SubscribeFilterLogs(ctx, query, ch) + + assert.NoError(t, err) + assert.Equal(t, sub, res) + mocks.MEVMCoreClient.AssertNumberOfCalls(t, "SubscribeFilterLogs", 3) +} + +func TestClientPool_BlockNumber(t *testing.T) { + setupCP() + ctx := context.TODO() + number := uint64(1) + mocks.MEVMCoreClient.On("BlockNumber", ctx).Return(uint64(0), errors.New("error")).Once(). + On("BlockNumber", ctx).Return(uint64(0), errors.New("error")).Once(). + On("BlockNumber", ctx).Return(number, nil).Once() + + res, err := cp.BlockNumber(ctx) + + assert.NoError(t, err) + assert.Equal(t, number, res) + mocks.MEVMCoreClient.AssertNumberOfCalls(t, "BlockNumber", 3) +} + +type MockSubscription struct { + mock.Mock +} + +func (m *MockSubscription) Unsubscribe() { + m.Called() +} + +func (m *MockSubscription) Err() <-chan error { + args := m.Called() + return args.Get(0).(chan error) +} + +func TestClientPool_PendingCodeAt(t *testing.T) { + setupCP() + ctx := context.TODO() + account := common.HexToAddress("0x123") + expectedCode := []byte{1, 2, 3} + mocks.MEVMCoreClient.On("PendingCodeAt", ctx, account).Return(nil, errors.New("error")).Once(). + On("PendingCodeAt", ctx, account).Return(nil, errors.New("error")).Once(). + On("PendingCodeAt", ctx, account).Return(expectedCode, nil).Once() + + actualCode, err := cp.PendingCodeAt(ctx, account) + + assert.NoError(t, err) + assert.Equal(t, expectedCode, actualCode) + mocks.MEVMCoreClient.AssertNumberOfCalls(t, "PendingCodeAt", 3) +} + +func TestClientPool_PendingNonceAt(t *testing.T) { + setupCP() + ctx := context.TODO() + account := common.HexToAddress("0x123") + expectedNonce := uint64(3) + mocks.MEVMCoreClient.On("PendingNonceAt", ctx, account).Return(uint64(0), errors.New("error")).Once(). + On("PendingNonceAt", ctx, account).Return(uint64(0), errors.New("error")).Once(). + On("PendingNonceAt", ctx, account).Return(expectedNonce, nil).Once() + + actualNonce, err := cp.PendingNonceAt(ctx, account) + + assert.NoError(t, err) + assert.Equal(t, expectedNonce, actualNonce) + mocks.MEVMCoreClient.AssertNumberOfCalls(t, "PendingNonceAt", 3) +} + +func TestClientPool_CallContract(t *testing.T) { + setupCP() + ctx := context.TODO() + call := ethereum.CallMsg{} + blockNumber := big.NewInt(1) + expectedResult := []byte{1, 2, 3} + mocks.MEVMCoreClient.On("CallContract", ctx, call, blockNumber).Return(nil, errors.New("error")).Once(). + On("CallContract", ctx, call, blockNumber).Return(nil, errors.New("error")).Once(). + On("CallContract", ctx, call, blockNumber).Return(expectedResult, nil).Once() + + actualResult, err := cp.CallContract(ctx, call, blockNumber) + + assert.NoError(t, err) + assert.Equal(t, expectedResult, actualResult) + mocks.MEVMCoreClient.AssertNumberOfCalls(t, "CallContract", 3) +} \ No newline at end of file diff --git a/bootstrap/clients.go b/bootstrap/clients.go index d95c2f02a..0198d5113 100644 --- a/bootstrap/clients.go +++ b/bootstrap/clients.go @@ -113,12 +113,12 @@ func bridgeCfgEventHandler(e event.Event, instance *Clients) error { func InitEVMClients(clientsCfg config.Clients, networks map[uint64]*parser.Network) map[uint64]client.EVM { EVMClients := make(map[uint64]client.EVM) - for configChainId, ec := range clientsCfg.Evm { + for configChainId, ec := range clientsCfg.EvmPool { network, ok := networks[configChainId] if !ok || network.RouterContractAddress == "" { continue } - EVMClients[configChainId] = evm.NewClient(ec, configChainId) + EVMClients[configChainId] = evm.NewClientPool(ec, configChainId) clientChainId, e := EVMClients[configChainId].ChainID(context.Background()) if e != nil { log.Fatalf("[%d] - Failed to retrieve chain ID on client prepare. Error: [%s]", configChainId, e) diff --git a/bootstrap/server.go b/bootstrap/server.go index c80f1e802..5f4143812 100644 --- a/bootstrap/server.go +++ b/bootstrap/server.go @@ -163,10 +163,10 @@ func registerEvmClients(server *server.Server, services *Services, repositories evmClient, services.Assets, dbIdentifier, - configuration.Node.Clients.Evm[chain].StartBlock, + configuration.Node.Clients.EvmPool[chain].StartBlock, configuration.Node.Validator, - configuration.Node.Clients.Evm[chain].PollingInterval, - configuration.Node.Clients.Evm[chain].MaxLogsBlocks, + configuration.Node.Clients.EvmPool[chain].PollingInterval, + configuration.Node.Clients.EvmPool[chain].MaxLogsBlocks, blacklisted, )) } diff --git a/config/node.go b/config/node.go index 62112be94..891643e93 100644 --- a/config/node.go +++ b/config/node.go @@ -25,13 +25,13 @@ import ( ) type Node struct { - Database Database - Clients Clients - LogLevel string - LogFormat string - Port string - Validator bool - Monitoring Monitoring + Database Database + Clients Clients + LogLevel string + LogFormat string + Port string + Validator bool + Monitoring Monitoring GaugeResetPassword string } @@ -44,7 +44,7 @@ type Database struct { } type Clients struct { - Evm map[uint64]Evm + EvmPool map[uint64]EvmPool Hedera Hedera MirrorNode MirrorNode CoinGecko CoinGecko @@ -52,8 +52,17 @@ type Clients struct { } type Evm struct { + BlockConfirmations uint64 `yaml:"block_confirmations"` + NodeUrl string `yaml:"node_url"` + PrivateKey string `yaml:"private_key"` + StartBlock int64 `yaml:"start_block"` + PollingInterval time.Duration `yaml:"polling_interval"` + MaxLogsBlocks int64 `yaml:"max_logs_blocks"` +} + +type EvmPool struct { BlockConfirmations uint64 - NodeUrl string + NodeUrls []string PrivateKey string StartBlock int64 PollingInterval time.Duration @@ -220,7 +229,7 @@ func New(node parser.Node) Node { Clients: Clients{ MirrorNode: *new(MirrorNode).DefaultOrConfig(&node.Clients.MirrorNode), Hedera: *new(Hedera).DefaultOrConfig(&node.Clients.Hedera), - Evm: make(map[uint64]Evm), + EvmPool: make(map[uint64]EvmPool), CoinGecko: CoinGecko{ ApiAddress: node.Clients.CoinGecko.ApiAddress, }, @@ -240,8 +249,8 @@ func New(node parser.Node) Node { GaugeResetPassword: node.GaugeResetPassword, } - for key, value := range node.Clients.Evm { - config.Clients.Evm[key] = Evm(value) + for key, value := range node.Clients.EvmPool { + config.Clients.EvmPool[key] = EvmPool(value) } return config diff --git a/config/node_test.go b/config/node_test.go index 96c96bc62..f962a6050 100644 --- a/config/node_test.go +++ b/config/node_test.go @@ -25,6 +25,8 @@ import ( ) func Test_New(t *testing.T) { + nodeUrls := []string{"node-url"} + in := parser.Node{ Database: parser.Database{ Host: "db-host", @@ -34,10 +36,10 @@ func Test_New(t *testing.T) { Username: "db-user", }, Clients: parser.Clients{ - Evm: map[uint64]parser.Evm{ + EvmPool: map[uint64]parser.EvmPool{ 80001: { BlockConfirmations: 1, - NodeUrl: "node-url", + NodeUrls: nodeUrls, PrivateKey: "private-key", StartBlock: 0, PollingInterval: 0, @@ -75,10 +77,10 @@ func Test_New(t *testing.T) { Username: "db-user", }, Clients: Clients{ - Evm: map[uint64]Evm{ + EvmPool: map[uint64]EvmPool{ 80001: { BlockConfirmations: 1, - NodeUrl: "node-url", + NodeUrls: nodeUrls, PrivateKey: "private-key", StartBlock: 0, PollingInterval: 0, diff --git a/config/parser/node.go b/config/parser/node.go index eb8ae8607..8ed6df510 100644 --- a/config/parser/node.go +++ b/config/parser/node.go @@ -42,11 +42,11 @@ type Database struct { } type Clients struct { - Evm map[uint64]Evm `yaml:"evm"` - Hedera Hedera `yaml:"hedera"` - MirrorNode MirrorNode `yaml:"mirror_node"` - CoinGecko CoinGecko `yaml:"coingecko"` - CoinMarketCap CoinMarketCap `yaml:"coin_market_cap"` + EvmPool map[uint64]EvmPool `yaml:"evm"` + Hedera Hedera `yaml:"hedera"` + MirrorNode MirrorNode `yaml:"mirror_node"` + CoinGecko CoinGecko `yaml:"coingecko"` + CoinMarketCap CoinMarketCap `yaml:"coin_market_cap"` } // Evm // @@ -60,6 +60,15 @@ type Evm struct { MaxLogsBlocks int64 `yaml:"max_logs_blocks"` } +type EvmPool struct { + BlockConfirmations uint64 `yaml:"block_confirmations"` + NodeUrls []string `yaml:"node_url"` + PrivateKey string `yaml:"private_key"` + StartBlock int64 `yaml:"start_block"` + PollingInterval time.Duration `yaml:"polling_interval"` + MaxLogsBlocks int64 `yaml:"max_logs_blocks"` +} + // Hedera // type Hedera struct { diff --git a/test/mocks/client/evm_core_client_mock.go b/test/mocks/client/evm_core_client_mock.go index 9593d587d..ec2e35e23 100644 --- a/test/mocks/client/evm_core_client_mock.go +++ b/test/mocks/client/evm_core_client_mock.go @@ -181,21 +181,78 @@ func (m *MockEVMCore) PendingCodeAt(ctx context.Context, account common.Address) } return args[0].([]byte), args[1].(error) } + func (m *MockEVMCore) PendingNonceAt(ctx context.Context, account common.Address) (uint64, error) { - panic("implement me") + args := m.Called(ctx, account) + if args[0] == nil && args[1] == nil { + return 0, nil + } + if args[0] == nil { + return 0, args[1].(error) + } + if args[1] == nil { + return args[0].(uint64), nil + } + return args[0].(uint64), args[1].(error) } + func (m *MockEVMCore) SuggestGasPrice(ctx context.Context) (*big.Int, error) { - panic("implement me") + args := m.Called(ctx) + if args[0] == nil && args[1] == nil { + return nil, nil + } + if args[0] == nil { + return nil, args[1].(error) + } + if args[1] == nil { + return args[0].(*big.Int), nil + } + return args[0].(*big.Int), args[1].(error) } + func (m *MockEVMCore) SuggestGasTipCap(ctx context.Context) (*big.Int, error) { - panic("implement me") + args := m.Called(ctx) + if args[0] == nil && args[1] == nil { + return nil, nil + } + if args[0] == nil { + return nil, args[1].(error) + } + if args[1] == nil { + return args[0].(*big.Int), nil + } + return args[0].(*big.Int), args[1].(error) } + func (m *MockEVMCore) EstimateGas(ctx context.Context, call ethereum.CallMsg) (gas uint64, err error) { - panic("implement me") + args := m.Called(ctx, call) + if args[0] == nil && args[1] == nil { + return 0, nil + } + if args[0] == nil { + return 0, args[1].(error) + } + if args[1] == nil { + return args[0].(uint64), nil + } + return args[0].(uint64), args[1].(error) } + func (m *MockEVMCore) SendTransaction(ctx context.Context, tx *types.Transaction) error { - panic("implement me") + args := m.Called(ctx, tx) + return args.Error(0) } + func (m *MockEVMCore) SubscribeFilterLogs(ctx context.Context, query ethereum.FilterQuery, ch chan<- types.Log) (ethereum.Subscription, error) { - panic("implement me") + args := m.Called(ctx, query, ch) + if args[0] == nil && args[1] == nil { + return nil, nil + } + if args[0] == nil { + return nil, args[1].(error) + } + if args[1] == nil { + return args[0].(ethereum.Subscription), nil + } + return args[0].(ethereum.Subscription), args[1].(error) } diff --git a/test/test-config/test-config.go b/test/test-config/test-config.go index 07ca21552..f86040373 100644 --- a/test/test-config/test-config.go +++ b/test/test-config/test-config.go @@ -24,6 +24,7 @@ import ( ) var ( + nodeUrls = []string{"wss://ropsten.infura.io/ws/v3/64364afbcf794ff9a00deabde636b7e1"} TestConfig = config.Config{ Node: config.Node{ LogLevel: "debug", @@ -37,9 +38,9 @@ var ( Username: "validator", }, Clients: config.Clients{ - Evm: map[uint64]config.Evm{ + EvmPool: map[uint64]config.EvmPool{ 3: { - NodeUrl: "wss://ropsten.infura.io/ws/v3/64364afbcf794ff9a00deabde636b7e1", + NodeUrls: nodeUrls, BlockConfirmations: 5, PrivateKey: "9f6da11eecc0fd7cb081d2aee88092ee3436397916c894ad6cd80a79009c0ded", },