code.vegaprotocol.io/vega@v0.79.0/core/datasource/external/ethcall/engine_test.go (about)

     1  // Copyright (C) 2023 Gobalsky Labs Limited
     2  //
     3  // This program is free software: you can redistribute it and/or modify
     4  // it under the terms of the GNU Affero General Public License as
     5  // published by the Free Software Foundation, either version 3 of the
     6  // License, or (at your option) any later version.
     7  //
     8  // This program is distributed in the hope that it will be useful,
     9  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    10  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    11  // GNU Affero General Public License for more details.
    12  //
    13  // You should have received a copy of the GNU Affero General Public License
    14  // along with this program.  If not, see <http://www.gnu.org/licenses/>.
    15  
    16  package ethcall_test
    17  
    18  import (
    19  	"context"
    20  	"math/big"
    21  	"strings"
    22  	"testing"
    23  	"time"
    24  
    25  	"code.vegaprotocol.io/vega/core/config/encoding"
    26  	"code.vegaprotocol.io/vega/core/datasource"
    27  	"code.vegaprotocol.io/vega/core/datasource/common"
    28  	"code.vegaprotocol.io/vega/core/datasource/external/ethcall"
    29  	ethcallcommon "code.vegaprotocol.io/vega/core/datasource/external/ethcall/common"
    30  	"code.vegaprotocol.io/vega/core/datasource/external/ethcall/mocks"
    31  	"code.vegaprotocol.io/vega/logging"
    32  	commandspb "code.vegaprotocol.io/vega/protos/vega/commands/v1"
    33  
    34  	"github.com/golang/mock/gomock"
    35  	"github.com/stretchr/testify/assert"
    36  	"github.com/stretchr/testify/require"
    37  )
    38  
    39  var TEST_CONFIG = ethcall.Config{
    40  	Level:                                   encoding.LogLevel{Level: logging.DebugLevel},
    41  	PollEvery:                               encoding.Duration{Duration: 100 * time.Second},
    42  	HeartbeatIntervalForTestOnlyDoNotChange: encoding.Duration{Duration: time.Hour},
    43  }
    44  
    45  func TestEngine(t *testing.T) {
    46  	ctx := context.Background()
    47  	tc, err := NewToyChain()
    48  	require.NoError(t, err)
    49  
    50  	ctrl := gomock.NewController(t)
    51  	forwarder := mocks.NewMockForwarder(ctrl)
    52  
    53  	log := logging.NewTestLogger()
    54  	e := ethcall.NewEngine(log, TEST_CONFIG, true, tc.client, forwarder)
    55  
    56  	currentEthTime := tc.client.Blockchain().CurrentBlock().Time
    57  
    58  	argsAsJson, err := ethcall.AnyArgsToJson([]any{big.NewInt(66)})
    59  	require.NoError(t, err)
    60  
    61  	ethCallSpec := &ethcallcommon.Spec{
    62  		Address:  tc.contractAddr.Hex(),
    63  		AbiJson:  tc.abiBytes,
    64  		Method:   "get_uint256",
    65  		ArgsJson: argsAsJson,
    66  		Trigger: ethcallcommon.TimeTrigger{
    67  			Initial: currentEthTime,
    68  			Every:   20,
    69  			Until:   0,
    70  		},
    71  
    72  		RequiredConfirmations: 0,
    73  		Filters:               common.SpecFilters{},
    74  	}
    75  
    76  	def := datasource.NewDefinitionWith(ethCallSpec)
    77  	oracleSpec := datasource.Spec{
    78  		ID:   "testid",
    79  		Data: def,
    80  		//},
    81  	}
    82  
    83  	err = e.OnSpecActivated(context.Background(), oracleSpec)
    84  
    85  	require.NoError(t, err)
    86  
    87  	// Make sure engine has a previous block to compare to
    88  	e.Poll(ctx, time.Now())
    89  
    90  	// Every commit advances chain time 10 seconds.
    91  	// This one shouldn't trigger our call because we're set to fire every 20 seconds
    92  	tc.client.Commit()
    93  	e.Poll(ctx, time.Now())
    94  
    95  	// But this one should
    96  	forwarder.EXPECT().ForwardFromSelf(gomock.Any()).Return().Do(func(ce *commandspb.ChainEvent) {
    97  		cc := ce.GetContractCall()
    98  		require.NotNil(t, cc)
    99  
   100  		assert.Equal(t, cc.BlockHeight, uint64(3))
   101  		assert.Equal(t, cc.BlockTime, uint64(30))
   102  		assert.Equal(t, cc.SpecId, "testid")
   103  	})
   104  	tc.client.Commit()
   105  	e.Poll(ctx, time.Now())
   106  
   107  	// Now try advancing advancing eth time 40 seconds through a two triggers and
   108  	// check that we get called twice given a single call to OnTick()
   109  	tc.client.Commit()
   110  	tc.client.Commit()
   111  	tc.client.Commit()
   112  	tc.client.Commit()
   113  
   114  	forwarder.EXPECT().ForwardFromSelf(gomock.Any()).Return().Do(func(ce *commandspb.ChainEvent) {
   115  		cc := ce.GetContractCall()
   116  		require.NotNil(t, cc)
   117  		assert.Equal(t, cc.BlockHeight, uint64(5))
   118  		assert.Equal(t, cc.BlockTime, uint64(50))
   119  		assert.False(t, cc.Heartbeat)
   120  	})
   121  
   122  	forwarder.EXPECT().ForwardFromSelf(gomock.Any()).Return().Do(func(ce *commandspb.ChainEvent) {
   123  		cc := ce.GetContractCall()
   124  		require.NotNil(t, cc)
   125  		assert.Equal(t, cc.BlockHeight, uint64(7))
   126  		assert.Equal(t, cc.BlockTime, uint64(70))
   127  		assert.False(t, cc.Heartbeat)
   128  	})
   129  
   130  	e.Poll(ctx, time.Now())
   131  
   132  	// Now deactivate the spec and make sure we don't get called again
   133  	tc.client.Commit()
   134  	tc.client.Commit()
   135  
   136  	e.OnSpecDeactivated(context.Background(), oracleSpec)
   137  	e.Poll(ctx, time.Now())
   138  }
   139  
   140  func TestEngineWithErrorSpec(t *testing.T) {
   141  	ctx := context.Background()
   142  	tc, err := NewToyChain()
   143  	require.NoError(t, err)
   144  
   145  	ctrl := gomock.NewController(t)
   146  	forwarder := mocks.NewMockForwarder(ctrl)
   147  
   148  	log := logging.NewTestLogger()
   149  	e := ethcall.NewEngine(log, TEST_CONFIG, true, tc.client, forwarder)
   150  
   151  	currentEthTime := tc.client.Blockchain().CurrentBlock().Time
   152  
   153  	argsAsJson, err := ethcall.AnyArgsToJson([]any{big.NewInt(66)})
   154  	require.NoError(t, err)
   155  
   156  	// To simulate a contract call error, we'll change the method name
   157  	tc.abiBytes = []byte(strings.Replace(string(tc.abiBytes), "get_uint256", "get_uint256doesnotexist", -1))
   158  
   159  	ethCallSpec := &ethcallcommon.Spec{
   160  		Address:  tc.contractAddr.Hex(),
   161  		AbiJson:  tc.abiBytes,
   162  		Method:   "get_uint256doesnotexist",
   163  		ArgsJson: argsAsJson,
   164  		Trigger: ethcallcommon.TimeTrigger{
   165  			Initial: currentEthTime,
   166  			Every:   20,
   167  			Until:   0,
   168  		},
   169  
   170  		RequiredConfirmations: 0,
   171  		Filters:               common.SpecFilters{},
   172  	}
   173  
   174  	def := datasource.NewDefinitionWith(ethCallSpec)
   175  	oracleSpec := datasource.Spec{
   176  		//&types.DataSourceSpec{
   177  		ID:   "testid",
   178  		Data: def,
   179  		//	},
   180  	}
   181  
   182  	err = e.OnSpecActivated(context.Background(), oracleSpec)
   183  
   184  	require.NoError(t, err)
   185  
   186  	// Make sure engine has a previous block to compare to
   187  	e.Poll(ctx, time.Now())
   188  
   189  	// Every commit advances chain time 10 seconds.
   190  	// This one shouldn't trigger our call because we're set to fire every 20 seconds
   191  	tc.client.Commit()
   192  	e.Poll(ctx, time.Now())
   193  
   194  	// But this one should
   195  	forwarder.EXPECT().ForwardFromSelf(gomock.Any()).Return().Do(func(ce *commandspb.ChainEvent) {
   196  		cc := ce.GetContractCall()
   197  		require.NotNil(t, cc)
   198  
   199  		assert.Equal(t, cc.BlockHeight, uint64(3))
   200  		assert.Equal(t, cc.BlockTime, uint64(30))
   201  		assert.Equal(t, cc.SpecId, "testid")
   202  		assert.False(t, cc.Heartbeat)
   203  		assert.NotNil(t, cc.Error)
   204  	})
   205  	tc.client.Commit()
   206  	e.Poll(ctx, time.Now())
   207  }
   208  
   209  func TestEngineHeartbeat(t *testing.T) {
   210  	ctx := context.Background()
   211  	tc, err := NewToyChain()
   212  	require.NoError(t, err)
   213  
   214  	cfg := ethcall.Config{
   215  		Level:                                   encoding.LogLevel{Level: logging.DebugLevel},
   216  		PollEvery:                               encoding.Duration{Duration: 100 * time.Second},
   217  		HeartbeatIntervalForTestOnlyDoNotChange: encoding.Duration{Duration: 45 * time.Second},
   218  	}
   219  
   220  	ctrl := gomock.NewController(t)
   221  	forwarder := mocks.NewMockForwarder(ctrl)
   222  
   223  	log := logging.NewTestLogger()
   224  	e := ethcall.NewEngine(log, cfg, true, tc.client, forwarder)
   225  
   226  	// we expect nothing to happen for the first few blocks
   227  	for i := 0; i < 5; i++ {
   228  		tc.client.Commit()
   229  		e.Poll(ctx, time.Now())
   230  	}
   231  
   232  	// but now we see a heartbeat
   233  	forwarder.EXPECT().ForwardFromSelf(gomock.Any()).Return().Do(func(ce *commandspb.ChainEvent) {
   234  		cc := ce.GetContractCall()
   235  		require.NotNil(t, cc)
   236  
   237  		assert.Equal(t, cc.BlockHeight, uint64(7))
   238  		assert.Equal(t, cc.BlockTime, uint64(70))
   239  		assert.True(t, cc.Heartbeat)
   240  		assert.Nil(t, cc.Error)
   241  	})
   242  	tc.client.Commit()
   243  	e.Poll(ctx, time.Now())
   244  }