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 := ðcallcommon.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 := ðcallcommon.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 }