diff --git a/.changeset/eleven-buckets-search.md b/.changeset/eleven-buckets-search.md new file mode 100644 index 00000000000..6c68fbcfdcc --- /dev/null +++ b/.changeset/eleven-buckets-search.md @@ -0,0 +1,5 @@ +--- +"chainlink": minor +--- + +#internal Add Log Poller support to Chain Reader through setting them in config. All filters should be part of the contract wide filter unless an event needs specific polling configuration, which can be set on a per event basis.. diff --git a/core/chains/evm/logpoller/orm.go b/core/chains/evm/logpoller/orm.go index 8a4c46bd7a6..b9a1ca39f4a 100644 --- a/core/chains/evm/logpoller/orm.go +++ b/core/chains/evm/logpoller/orm.go @@ -971,6 +971,7 @@ func (o *DSORM) SelectIndexedLogsWithSigsExcluding(ctx context.Context, sigA, si return logs, nil } +// TODO flaky BCF-3258 func (o *DSORM) FilteredLogs(ctx context.Context, filter query.KeyFilter, limitAndSort query.LimitAndSort, _ string) ([]Log, error) { qs, args, err := (&pgDSLParser{}).buildQuery(o.chainID, filter.Expressions, limitAndSort) if err != nil { diff --git a/core/internal/features/ocr2/features_ocr2_test.go b/core/internal/features/ocr2/features_ocr2_test.go index ae87baa564e..440f68d8931 100644 --- a/core/internal/features/ocr2/features_ocr2_test.go +++ b/core/internal/features/ocr2/features_ocr2_test.go @@ -319,6 +319,8 @@ fromBlock = %d if test.chainReaderAndCodec { chainReaderSpec = ` [relayConfig.chainReader.contracts.median] +contractPollingFilter.genericEventNames = ["LatestRoundRequested"] + contractABI = ''' [ { diff --git a/core/services/relay/evm/binding.go b/core/services/relay/evm/binding.go index d7a04dcc9b9..9c7fd186dec 100644 --- a/core/services/relay/evm/binding.go +++ b/core/services/relay/evm/binding.go @@ -8,10 +8,10 @@ import ( ) type readBinding interface { - GetLatestValue(ctx context.Context, params, returnVal any) error - QueryKey(ctx context.Context, filter query.KeyFilter, limitAndSort query.LimitAndSort, sequenceDataType any) ([]commontypes.Sequence, error) Bind(ctx context.Context, binding commontypes.BoundContract) error SetCodec(codec commontypes.RemoteCodec) Register(ctx context.Context) error Unregister(ctx context.Context) error + GetLatestValue(ctx context.Context, params, returnVal any) error + QueryKey(ctx context.Context, filter query.KeyFilter, limitAndSort query.LimitAndSort, sequenceDataType any) ([]commontypes.Sequence, error) } diff --git a/core/services/relay/evm/bindings.go b/core/services/relay/evm/bindings.go index e13fcbc02d5..9ad73c01926 100644 --- a/core/services/relay/evm/bindings.go +++ b/core/services/relay/evm/bindings.go @@ -5,44 +5,54 @@ import ( "fmt" commontypes "github.com/smartcontractkit/chainlink-common/pkg/types" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller" ) -// key is contract name -type contractBindings map[string]readBindings +// bindings manage all contract bindings, key is contract name. +type bindings map[string]*contractBinding -// key is read name -type readBindings map[string]readBinding - -func (b contractBindings) GetReadBinding(contractName, readName string) (readBinding, error) { - rb, rbExists := b[contractName] - if !rbExists { +func (b bindings) GetReadBinding(contractName, readName string) (readBinding, error) { + // GetReadBindings should only be called after Chain Reader init. + cb, cbExists := b[contractName] + if !cbExists { return nil, fmt.Errorf("%w: no contract named %s", commontypes.ErrInvalidType, contractName) } - reader, readerExists := rb[readName] - if !readerExists { + rb, rbExists := cb.readBindings[readName] + if !rbExists { return nil, fmt.Errorf("%w: no readName named %s in contract %s", commontypes.ErrInvalidType, readName, contractName) } - return reader, nil + return rb, nil } -func (b contractBindings) AddReadBinding(contractName, readName string, reader readBinding) { - rbs, rbsExists := b[contractName] - if !rbsExists { - rbs = readBindings{} - b[contractName] = rbs +// AddReadBinding adds read bindings. Calling this outside of Chain Reader init is not thread safe. +func (b bindings) AddReadBinding(contractName, readName string, rb readBinding) { + cb, cbExists := b[contractName] + if !cbExists { + cb = &contractBinding{ + name: contractName, + readBindings: make(map[string]readBinding), + } + b[contractName] = cb } - rbs[readName] = reader + cb.readBindings[readName] = rb } -func (b contractBindings) Bind(ctx context.Context, boundContracts []commontypes.BoundContract) error { +// Bind binds contract addresses to contract bindings and read bindings. +// Bind also registers the common contract polling filter and eventBindings polling filters. +func (b bindings) Bind(ctx context.Context, lp logpoller.LogPoller, boundContracts []commontypes.BoundContract) error { for _, bc := range boundContracts { - rbs, rbsExist := b[bc.Name] - if !rbsExist { + cb, cbExists := b[bc.Name] + if !cbExists { return fmt.Errorf("%w: no contract named %s", commontypes.ErrInvalidConfig, bc.Name) } - for _, r := range rbs { - if err := r.Bind(ctx, bc); err != nil { + + if err := cb.Bind(ctx, lp, bc); err != nil { + return err + } + + for _, rb := range cb.readBindings { + if err := rb.Bind(ctx, bc); err != nil { return err } } @@ -50,12 +60,10 @@ func (b contractBindings) Bind(ctx context.Context, boundContracts []commontypes return nil } -func (b contractBindings) ForEach(ctx context.Context, fn func(readBinding, context.Context) error) error { - for _, rbs := range b { - for _, rb := range rbs { - if err := fn(rb, ctx); err != nil { - return err - } +func (b bindings) ForEach(ctx context.Context, fn func(context.Context, *contractBinding) error) error { + for _, cb := range b { + if err := fn(ctx, cb); err != nil { + return err } } return nil diff --git a/core/services/relay/evm/chain_reader.go b/core/services/relay/evm/chain_reader.go index bd769e05f37..9cfe846d55d 100644 --- a/core/services/relay/evm/chain_reader.go +++ b/core/services/relay/evm/chain_reader.go @@ -5,11 +5,12 @@ import ( "errors" "fmt" "reflect" + "slices" "strings" + "sync" "time" "github.com/ethereum/go-ethereum/accounts/abi" - "github.com/google/uuid" "github.com/smartcontractkit/chainlink-common/pkg/codec" "github.com/smartcontractkit/chainlink-common/pkg/types/query" @@ -35,13 +36,14 @@ type chainReader struct { lggr logger.Logger lp logpoller.LogPoller client evmclient.Client - contractBindings contractBindings + contractBindings bindings parsed *parsedTypes codec commontypes.RemoteCodec commonservices.StateMachine } var _ ChainReaderService = (*chainReader)(nil) +var _ commontypes.ContractTypeProvider = &chainReader{} // NewChainReaderService is a constructor for ChainReader, returns nil if there is any error // Note that the ChainReaderService returned does not support anonymous events. @@ -50,7 +52,7 @@ func NewChainReaderService(ctx context.Context, lggr logger.Logger, lp logpoller lggr: lggr.Named("ChainReader"), lp: lp, client: client, - contractBindings: contractBindings{}, + contractBindings: bindings{}, parsed: &parsedTypes{encoderDefs: map[string]types.CodecEntry{}, decoderDefs: map[string]types.CodecEntry{}}, } @@ -63,52 +65,48 @@ func NewChainReaderService(ctx context.Context, lggr logger.Logger, lp logpoller return nil, err } - err = cr.contractBindings.ForEach(ctx, func(b readBinding, c context.Context) error { - b.SetCodec(cr.codec) + err = cr.contractBindings.ForEach(ctx, func(c context.Context, cb *contractBinding) error { + for _, rb := range cb.readBindings { + rb.SetCodec(cr.codec) + } return nil }) return cr, err } -func (cr *chainReader) Name() string { return cr.lggr.Name() } - -var _ commontypes.ContractTypeProvider = &chainReader{} - -func (cr *chainReader) GetLatestValue(ctx context.Context, contractName, method string, params any, returnVal any) error { - b, err := cr.contractBindings.GetReadBinding(contractName, method) - if err != nil { - return err - } - - return b.GetLatestValue(ctx, params, returnVal) -} - -func (cr *chainReader) Bind(ctx context.Context, bindings []commontypes.BoundContract) error { - return cr.contractBindings.Bind(ctx, bindings) -} - -func (cr *chainReader) QueryKey(ctx context.Context, contractName string, filter query.KeyFilter, limitAndSort query.LimitAndSort, sequenceDataType any) ([]commontypes.Sequence, error) { - b, err := cr.contractBindings.GetReadBinding(contractName, filter.Key) - if err != nil { - return nil, err - } - - return b.QueryKey(ctx, filter, limitAndSort, sequenceDataType) -} - func (cr *chainReader) init(chainContractReaders map[string]types.ChainContractReader) error { for contractName, chainContractReader := range chainContractReaders { contractAbi, err := abi.JSON(strings.NewReader(chainContractReader.ContractABI)) if err != nil { - return err + return fmt.Errorf("failed to parse abi for contract: %s, err: %w", contractName, err) } + var eventSigsForContractFilter evmtypes.HashArray for typeName, chainReaderDefinition := range chainContractReader.Configs { switch chainReaderDefinition.ReadType { case types.Method: err = cr.addMethod(contractName, typeName, contractAbi, *chainReaderDefinition) case types.Event: + partOfContractCommonFilter := slices.Contains(chainContractReader.GenericEventNames, typeName) + if !partOfContractCommonFilter && !chainReaderDefinition.HasPollingFilter() { + return fmt.Errorf( + "%w: chain reader has no polling filter defined for contract: %s, event: %s", + commontypes.ErrInvalidConfig, contractName, typeName) + } + + eventOverridesContractFilter := chainReaderDefinition.HasPollingFilter() + if eventOverridesContractFilter && partOfContractCommonFilter { + return fmt.Errorf( + "%w: conflicting chain reader polling filter definitions for contract: %s event: %s, can't have polling filter defined both on contract and event level", + commontypes.ErrInvalidConfig, contractName, typeName) + } + + if !eventOverridesContractFilter && + !slices.Contains(eventSigsForContractFilter, contractAbi.Events[chainReaderDefinition.ChainSpecificName].ID) { + eventSigsForContractFilter = append(eventSigsForContractFilter, contractAbi.Events[chainReaderDefinition.ChainSpecificName].ID) + } + err = cr.addEvent(contractName, typeName, contractAbi, *chainReaderDefinition) default: return fmt.Errorf( @@ -116,34 +114,75 @@ func (cr *chainReader) init(chainContractReaders map[string]types.ChainContractR commontypes.ErrInvalidConfig, chainReaderDefinition.ReadType) } - if err != nil { return err } } + cr.contractBindings[contractName].pollingFilter = chainContractReader.PollingFilter.ToLPFilter(eventSigsForContractFilter) } return nil } +func (cr *chainReader) Name() string { return cr.lggr.Name() } + +// Start registers polling filters if contracts are already bound. func (cr *chainReader) Start(ctx context.Context) error { return cr.StartOnce("ChainReader", func() error { - return cr.contractBindings.ForEach(ctx, readBinding.Register) + return cr.contractBindings.ForEach(ctx, func(c context.Context, cb *contractBinding) error { + for _, rb := range cb.readBindings { + if err := rb.Register(ctx); err != nil { + return err + } + } + return cb.Register(ctx, cr.lp) + }) }) } +// Close unregisters polling filters for bound contracts. func (cr *chainReader) Close() error { return cr.StopOnce("ChainReader", func() error { ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() - return cr.contractBindings.ForEach(ctx, readBinding.Unregister) + return cr.contractBindings.ForEach(ctx, func(c context.Context, cb *contractBinding) error { + for _, rb := range cb.readBindings { + if err := rb.Unregister(ctx); err != nil { + return err + } + } + return cb.Unregister(ctx, cr.lp) + }) }) } func (cr *chainReader) Ready() error { return nil } + func (cr *chainReader) HealthReport() map[string]error { return map[string]error{cr.Name(): nil} } +func (cr *chainReader) GetLatestValue(ctx context.Context, contractName, method string, params any, returnVal any) error { + b, err := cr.contractBindings.GetReadBinding(contractName, method) + if err != nil { + return err + } + + return b.GetLatestValue(ctx, params, returnVal) +} + +func (cr *chainReader) Bind(ctx context.Context, bindings []commontypes.BoundContract) error { + return cr.contractBindings.Bind(ctx, cr.lp, bindings) +} + +func (cr *chainReader) QueryKey(ctx context.Context, contractName string, filter query.KeyFilter, limitAndSort query.LimitAndSort, sequenceDataType any) ([]commontypes.Sequence, error) { + b, err := cr.contractBindings.GetReadBinding(contractName, filter.Key) + if err != nil { + return nil, err + } + + return b.QueryKey(ctx, filter, limitAndSort, sequenceDataType) +} + func (cr *chainReader) CreateContractType(contractName, itemType string, forEncoding bool) (any, error) { return cr.codec.CreateType(wrapItemType(contractName, itemType, forEncoding), forEncoding) } @@ -165,13 +204,6 @@ func (cr *chainReader) addMethod( return fmt.Errorf("%w: method %s doesn't exist", commontypes.ErrInvalidConfig, chainReaderDefinition.ChainSpecificName) } - if len(chainReaderDefinition.EventInputFields) != 0 { - return fmt.Errorf( - "%w: method %s has event topic fields defined, but is not an event", - commontypes.ErrInvalidConfig, - chainReaderDefinition.ChainSpecificName) - } - cr.contractBindings.AddReadBinding(contractName, methodName, &methodBinding{ contractName: contractName, method: methodName, @@ -191,8 +223,13 @@ func (cr *chainReader) addEvent(contractName, eventName string, a abi.ABI, chain return fmt.Errorf("%w: event %s doesn't exist", commontypes.ErrInvalidConfig, chainReaderDefinition.ChainSpecificName) } - filterArgs, codecTopicInfo, indexArgNames := setupEventInput(event, chainReaderDefinition) - if err := verifyEventInputsUsed(chainReaderDefinition, indexArgNames); err != nil { + var inputFields []string + if chainReaderDefinition.EventDefinitions != nil { + inputFields = chainReaderDefinition.EventDefinitions.InputFields + } + + filterArgs, codecTopicInfo, indexArgNames := setupEventInput(event, inputFields) + if err := verifyEventIndexedInputsUsed(eventName, inputFields, indexArgNames); err != nil { return err } @@ -224,34 +261,48 @@ func (cr *chainReader) addEvent(contractName, eventName string, a abi.ABI, chain inputModifier: inputModifier, codecTopicInfo: codecTopicInfo, topics: make(map[string]topicDetail), - eventDataWords: chainReaderDefinition.GenericDataWordNames, - id: wrapItemType(contractName, eventName, false) + uuid.NewString(), + eventDataWords: make(map[string]uint8), confirmationsMapping: confirmations, } + if eventDefinitions := chainReaderDefinition.EventDefinitions; eventDefinitions != nil { + if eventDefinitions.PollingFilter != nil { + eb.filterRegisterer = &filterRegisterer{ + pollingFilter: eventDefinitions.PollingFilter.ToLPFilter(evmtypes.HashArray{a.Events[event.Name].ID}), + filterLock: sync.Mutex{}, + } + } + + if eventDefinitions.GenericDataWordNames != nil { + eb.eventDataWords = eventDefinitions.GenericDataWordNames + } + + cr.addQueryingReadBindings(contractName, eventDefinitions.GenericTopicNames, event.Inputs, eb) + } + cr.contractBindings.AddReadBinding(contractName, eventName, eb) - // set topic mappings for QueryKeys - for topicIndex, topic := range event.Inputs { - genericTopicName, ok := chainReaderDefinition.GenericTopicNames[topic.Name] + return cr.addDecoderDef(contractName, eventName, event.Inputs, chainReaderDefinition) +} + +// addQueryingReadBindings reuses the eventBinding and maps it to topic and dataWord keys used for QueryKey. +func (cr *chainReader) addQueryingReadBindings(contractName string, genericTopicNames map[string]string, eventInputs abi.Arguments, eb *eventBinding) { + // add topic readBindings for QueryKey + for topicIndex, topic := range eventInputs { + genericTopicName, ok := genericTopicNames[topic.Name] if ok { eb.topics[genericTopicName] = topicDetail{ Argument: topic, Index: uint64(topicIndex), } } - - // this way querying by key/s values comparison can find its bindings cr.contractBindings.AddReadBinding(contractName, genericTopicName, eb) } - // set data word mappings for QueryKeys + // add data word readBindings for QueryKey for genericDataWordName := range eb.eventDataWords { - // this way querying by key/s values comparison can find its bindings cr.contractBindings.AddReadBinding(contractName, genericDataWordName, eb) } - - return cr.addDecoderDef(contractName, eventName, event.Inputs, chainReaderDefinition) } func (cr *chainReader) getEventInput(def types.ChainReaderDefinition, contractName, eventName string) ( @@ -270,10 +321,10 @@ func (cr *chainReader) getEventInput(def types.ChainReaderDefinition, contractNa return inputInfo, inMod, nil } -func verifyEventInputsUsed(chainReaderDefinition types.ChainReaderDefinition, indexArgNames map[string]bool) error { - for _, value := range chainReaderDefinition.EventInputFields { +func verifyEventIndexedInputsUsed(eventName string, inputFields []string, indexArgNames map[string]bool) error { + for _, value := range inputFields { if !indexArgNames[abi.ToCamelCase(value)] { - return fmt.Errorf("%w: %s is not an indexed argument of event %s", commontypes.ErrInvalidConfig, value, chainReaderDefinition.ChainSpecificName) + return fmt.Errorf("%w: %s is not an indexed argument of event %s", commontypes.ErrInvalidConfig, value, eventName) } } return nil @@ -305,9 +356,9 @@ func (cr *chainReader) addDecoderDef(contractName, itemType string, outputs abi. return output.Init() } -func setupEventInput(event abi.Event, def types.ChainReaderDefinition) ([]abi.Argument, types.CodecEntry, map[string]bool) { +func setupEventInput(event abi.Event, inputFields []string) ([]abi.Argument, types.CodecEntry, map[string]bool) { topicFieldDefs := map[string]bool{} - for _, value := range def.EventInputFields { + for _, value := range inputFields { capFirstValue := abi.ToCamelCase(value) topicFieldDefs[capFirstValue] = true } diff --git a/core/services/relay/evm/chain_reader_test.go b/core/services/relay/evm/chain_reader_test.go index 9e133428bf3..de812262f4a 100644 --- a/core/services/relay/evm/chain_reader_test.go +++ b/core/services/relay/evm/chain_reader_test.go @@ -15,9 +15,14 @@ import ( "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/crypto" "github.com/jmoiron/sqlx" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + clcommontypes "github.com/smartcontractkit/chainlink-common/pkg/types" . "github.com/smartcontractkit/chainlink-common/pkg/types/interfacetests" //nolint common practice to import test mods with . + "github.com/smartcontractkit/chainlink/v2/core/logger" + "github.com/smartcontractkit/chainlink/v2/core/services/relay/evm" + "github.com/smartcontractkit/chainlink/v2/core/services/relay/evm/types" commontestutils "github.com/smartcontractkit/chainlink-common/pkg/loop/testutils" @@ -29,7 +34,114 @@ import ( const commonGasLimitOnEvms = uint64(4712388) +func TestChainReaderEventsInitValidation(t *testing.T) { + tests := []struct { + name string + chainContractReaders map[string]types.ChainContractReader + expectedError error + }{ + { + name: "Invalid ABI", + chainContractReaders: map[string]types.ChainContractReader{ + "InvalidContract": { + ContractABI: "{invalid json}", + Configs: map[string]*types.ChainReaderDefinition{}, + }, + }, + expectedError: fmt.Errorf("failed to parse abi"), + }, + { + name: "Conflicting polling filter definitions", + chainContractReaders: map[string]types.ChainContractReader{ + "ContractWithConflict": { + ContractABI: "[]", + Configs: map[string]*types.ChainReaderDefinition{ + "EventWithConflict": { + ChainSpecificName: "EventName", + ReadType: types.Event, + EventDefinitions: &types.EventDefinitions{ + PollingFilter: &types.PollingFilter{}, + }, + }, + }, + ContractPollingFilter: types.ContractPollingFilter{ + GenericEventNames: []string{"EventWithConflict"}, + }, + }, + }, + expectedError: fmt.Errorf( + "%w: conflicting chain reader polling filter definitions for contract: %s event: %s, can't have polling filter defined both on contract and event level", + clcommontypes.ErrInvalidConfig, "ContractWithConflict", "EventWithConflict"), + }, + { + name: "No polling filter defined", + chainContractReaders: map[string]types.ChainContractReader{ + "ContractWithNoFilter": { + ContractABI: "[]", + Configs: map[string]*types.ChainReaderDefinition{ + "EventWithNoFilter": { + ChainSpecificName: "EventName", + ReadType: types.Event, + }, + }, + }, + }, + expectedError: fmt.Errorf( + "%w: chain reader has no polling filter defined for contract: %s, event: %s", + clcommontypes.ErrInvalidConfig, "ContractWithNoFilter", "EventWithNoFilter"), + }, + { + name: "Invalid chain reader definition read type", + chainContractReaders: map[string]types.ChainContractReader{ + "ContractWithInvalidReadType": { + ContractABI: "[]", + Configs: map[string]*types.ChainReaderDefinition{ + "InvalidReadType": { + ChainSpecificName: "InvalidName", + ReadType: types.ReadType(2), + }, + }, + }, + }, + expectedError: fmt.Errorf( + "%w: invalid chain reader definition read type", + clcommontypes.ErrInvalidConfig), + }, + { + name: "Event not present in ABI", + chainContractReaders: map[string]types.ChainContractReader{ + "ContractWithConflict": { + ContractABI: "[{\"anonymous\":false,\"inputs\":[],\"name\":\"WrongEvent\",\"type\":\"event\"}]", + Configs: map[string]*types.ChainReaderDefinition{ + "SomeEvent": { + ChainSpecificName: "EventName", + ReadType: types.Event, + }, + }, + ContractPollingFilter: types.ContractPollingFilter{ + GenericEventNames: []string{"SomeEvent"}, + }, + }, + }, + expectedError: fmt.Errorf( + "%w: event %s doesn't exist", + clcommontypes.ErrInvalidConfig, "EventName"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := evm.NewChainReaderService(testutils.Context(t), logger.NullLogger, nil, nil, types.ChainReaderConfig{Contracts: tt.chainContractReaders}) + require.Error(t, err) + if err != nil { + assert.Contains(t, err.Error(), tt.expectedError.Error()) + } + }) + } +} + func TestChainReader(t *testing.T) { + // TODO QueryKey test is flaky BCF-3258 t.Parallel() it := &EVMChainReaderInterfaceTester[*testing.T]{Helper: &helper{}} RunChainReaderEvmTests(t, it) diff --git a/core/services/relay/evm/contract_binding.go b/core/services/relay/evm/contract_binding.go new file mode 100644 index 00000000000..da2d7ed9bd1 --- /dev/null +++ b/core/services/relay/evm/contract_binding.go @@ -0,0 +1,100 @@ +package evm + +import ( + "context" + "fmt" + "sync" + + "github.com/ethereum/go-ethereum/common" + "github.com/google/uuid" + + commontypes "github.com/smartcontractkit/chainlink-common/pkg/types" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller" + evmtypes "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" +) + +type filterRegisterer struct { + pollingFilter logpoller.Filter + filterLock sync.Mutex + // registerCalled is used to determine if Register was called during Chain Reader service Start. + // This is done to avoid calling Register while the service is not running because log poller is most likely also not running. + registerCalled bool +} + +// contractBinding stores read bindings and manages the common contract event filter. +type contractBinding struct { + name string + // filterRegisterer is used to manage polling filter registration for the common contract filter. + // The common contract filter should be used by events that share filtering args. + filterRegisterer + // key is read name method, event or event keys used for queryKey. + readBindings map[string]readBinding + // bound determines if address is set to the contract binding. + bound bool + bindLock sync.Mutex +} + +// Bind binds contract addresses to contract binding and registers the common contract polling filter. +func (cb *contractBinding) Bind(ctx context.Context, lp logpoller.LogPoller, boundContract commontypes.BoundContract) error { + // it's enough to just lock bound here since Register/Unregister are only called from here and from Start/Close + // even if they somehow happen at the same time it will be fine because of filter lock and hasFilter check + cb.bindLock.Lock() + defer cb.bindLock.Unlock() + + if cb.bound { + // we are changing contract address reference, so we need to unregister old filter it exists + if err := cb.Unregister(ctx, lp); err != nil { + return err + } + } + + cb.pollingFilter.Addresses = evmtypes.AddressArray{common.HexToAddress(boundContract.Address)} + cb.pollingFilter.Name = logpoller.FilterName(boundContract.Name+"."+uuid.NewString(), boundContract.Address) + cb.bound = true + + if cb.registerCalled { + return cb.Register(ctx, lp) + } + + return nil +} + +// Register registers the common contract filter. +func (cb *contractBinding) Register(ctx context.Context, lp logpoller.LogPoller) error { + cb.filterLock.Lock() + defer cb.filterLock.Unlock() + + cb.registerCalled = true + // can't be true before filters params are set so there is no race with a bad filter outcome + if !cb.bound { + return nil + } + + if len(cb.pollingFilter.EventSigs) > 0 && !lp.HasFilter(cb.pollingFilter.Name) { + if err := lp.RegisterFilter(ctx, cb.pollingFilter); err != nil { + return fmt.Errorf("%w: %w", commontypes.ErrInternal, err) + } + } + + return nil +} + +// Unregister unregisters the common contract filter. +func (cb *contractBinding) Unregister(ctx context.Context, lp logpoller.LogPoller) error { + cb.filterLock.Lock() + defer cb.filterLock.Unlock() + + if !cb.bound { + return nil + } + + if !lp.HasFilter(cb.pollingFilter.Name) { + return nil + } + + if err := lp.UnregisterFilter(ctx, cb.pollingFilter.Name); err != nil { + return fmt.Errorf("%w: %w", commontypes.ErrInternal, err) + } + + return nil +} diff --git a/core/services/relay/evm/event_binding.go b/core/services/relay/evm/event_binding.go index f7f628f6bd7..a3f1fa9d6a2 100644 --- a/core/services/relay/evm/event_binding.go +++ b/core/services/relay/evm/event_binding.go @@ -9,6 +9,7 @@ import ( "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" + "github.com/google/uuid" "github.com/smartcontractkit/chainlink-common/pkg/codec" commontypes "github.com/smartcontractkit/chainlink-common/pkg/types" @@ -21,16 +22,19 @@ import ( ) type eventBinding struct { - address common.Address - contractName string - eventName string - lp logpoller.LogPoller - hash common.Hash - codec commontypes.RemoteCodec - pending bool + address common.Address + contractName string + eventName string + lp logpoller.LogPoller + // filterRegisterer in eventBinding is to be used as an override for lp filter defined in the contract binding. + // If filterRegisterer is nil, this event should be registered with the lp filter defined in the contract binding. + *filterRegisterer + hash common.Hash + codec commontypes.RemoteCodec + pending bool + // bound determines if address is set to the contract binding. bound bool - registerCalled bool - lock sync.Mutex + bindLock sync.Mutex inputInfo types.CodecEntry inputModifier codec.Modifier codecTopicInfo types.CodecEntry @@ -38,9 +42,8 @@ type eventBinding struct { topics map[string]topicDetail // eventDataWords maps a generic name to a word index // key is a predefined generic name for evm log event data word - // for eg. first evm data word(32bytes) of USDC log event is value so the key can be called value + // for e.g. first evm data word(32bytes) of USDC log event is value so the key can be called value eventDataWords map[string]uint8 - id string confirmationsMapping map[primitives.ConfidenceLevel]evmtypes.Confirmations } @@ -55,44 +58,90 @@ func (e *eventBinding) SetCodec(codec commontypes.RemoteCodec) { e.codec = codec } +func (e *eventBinding) Bind(ctx context.Context, binding commontypes.BoundContract) error { + // it's enough to just lock bound here since Register/Unregister are only called from here and from Start/Close + // even if they somehow happen at the same time it will be fine because of filter lock and hasFilter check + e.bindLock.Lock() + defer e.bindLock.Unlock() + + if e.bound { + // we are changing contract address reference, so we need to unregister old filter it exists + if err := e.Unregister(ctx); err != nil { + return err + } + } + + e.address = common.HexToAddress(binding.Address) + e.pending = binding.Pending + + // filterRegisterer isn't required here because the event can also be polled for by the contractBinding common filter. + if e.filterRegisterer != nil { + id := fmt.Sprintf("%s.%s.%s", e.contractName, e.eventName, uuid.NewString()) + e.pollingFilter.Name = logpoller.FilterName(id, e.address) + e.pollingFilter.Addresses = evmtypes.AddressArray{e.address} + e.bound = true + if e.registerCalled { + return e.Register(ctx) + } + } + e.bound = true + return nil +} + func (e *eventBinding) Register(ctx context.Context) error { - e.lock.Lock() - defer e.lock.Unlock() + if e.filterRegisterer == nil { + return nil + } + + e.filterLock.Lock() + defer e.filterLock.Unlock() e.registerCalled = true - if !e.bound || e.lp.HasFilter(e.id) { + // can't be true before filters params are set so there is no race with a bad filter outcome + if !e.bound { return nil } - if err := e.lp.RegisterFilter(ctx, logpoller.Filter{ - Name: e.id, - EventSigs: evmtypes.HashArray{e.hash}, - Addresses: evmtypes.AddressArray{e.address}, - }); err != nil { + if e.lp.HasFilter(e.pollingFilter.Name) { + return nil + } + + if err := e.lp.RegisterFilter(ctx, e.pollingFilter); err != nil { return fmt.Errorf("%w: %w", commontypes.ErrInternal, err) } + return nil } func (e *eventBinding) Unregister(ctx context.Context) error { - e.lock.Lock() - defer e.lock.Unlock() + if e.filterRegisterer == nil { + return nil + } + + e.filterLock.Lock() + defer e.filterLock.Unlock() - if !e.lp.HasFilter(e.id) { + if !e.bound { return nil } - if err := e.lp.UnregisterFilter(ctx, e.id); err != nil { + if !e.lp.HasFilter(e.pollingFilter.Name) { + return nil + } + + if err := e.lp.UnregisterFilter(ctx, e.pollingFilter.Name); err != nil { return fmt.Errorf("%w: %w", commontypes.ErrInternal, err) } + return nil } func (e *eventBinding) GetLatestValue(ctx context.Context, params, into any) error { - if !e.bound { - return fmt.Errorf("%w: event not bound", commontypes.ErrInvalidType) + if err := e.validateBound(); err != nil { + return err } + // TODO BCF-3247 change GetLatestValue to use chain agnostic confidence levels confs := evmtypes.Finalized if e.pending { confs = evmtypes.Unconfirmed @@ -106,8 +155,8 @@ func (e *eventBinding) GetLatestValue(ctx context.Context, params, into any) err } func (e *eventBinding) QueryKey(ctx context.Context, filter query.KeyFilter, limitAndSort query.LimitAndSort, sequenceDataType any) ([]commontypes.Sequence, error) { - if !e.bound { - return nil, fmt.Errorf("%w: event not bound", commontypes.ErrInvalidType) + if err := e.validateBound(); err != nil { + return nil, err } remapped, err := e.remap(filter) @@ -135,17 +184,14 @@ func (e *eventBinding) QueryKey(ctx context.Context, filter query.KeyFilter, lim return e.decodeLogsIntoSequences(ctx, logs, sequenceDataType) } -func (e *eventBinding) Bind(ctx context.Context, binding commontypes.BoundContract) error { - if err := e.Unregister(ctx); err != nil { - return err - } - - e.address = common.HexToAddress(binding.Address) - e.pending = binding.Pending - e.bound = true - - if e.registerCalled { - return e.Register(ctx) +func (e *eventBinding) validateBound() error { + if !e.bound { + return fmt.Errorf( + "%w: event %s that belongs to contract: %s, not bound", + commontypes.ErrInvalidType, + e.eventName, + e.contractName, + ) } return nil } diff --git a/core/services/relay/evm/evmtesting/chain_reader_interface_tester.go b/core/services/relay/evm/evmtesting/chain_reader_interface_tester.go index a2c9180d3bf..61a45996ac2 100644 --- a/core/services/relay/evm/evmtesting/chain_reader_interface_tester.go +++ b/core/services/relay/evm/evmtesting/chain_reader_interface_tester.go @@ -83,6 +83,9 @@ func (it *EVMChainReaderInterfaceTester[T]) Setup(t T) { Contracts: map[string]types.ChainContractReader{ AnyContractName: { ContractABI: chain_reader_tester.ChainReaderTesterMetaData.ABI, + ContractPollingFilter: types.ContractPollingFilter{ + GenericEventNames: []string{EventName, EventWithFilterName}, + }, Configs: map[string]*types.ChainReaderDefinition{ MethodTakingLatestParamsReturningTestStruct: { ChainSpecificName: "getElementAtIndex", @@ -110,22 +113,30 @@ func (it *EVMChainReaderInterfaceTester[T]) Setup(t T) { EventWithFilterName: { ChainSpecificName: "Triggered", ReadType: types.Event, - EventInputFields: []string{"Field"}, + EventDefinitions: &types.EventDefinitions{InputFields: []string{"Field"}}, ConfidenceConfirmations: map[string]int{"0.0": 0, "1.0": -1}, }, triggerWithDynamicTopic: { ChainSpecificName: triggerWithDynamicTopic, ReadType: types.Event, - EventInputFields: []string{"fieldHash"}, + EventDefinitions: &types.EventDefinitions{ + InputFields: []string{"fieldHash"}, + // no specific reason for filter being defined here insted on contract level, + // this is just for test case variety + PollingFilter: &types.PollingFilter{}, + }, InputModifications: codec.ModifiersConfig{ &codec.RenameModifierConfig{Fields: map[string]string{"FieldHash": "Field"}}, }, ConfidenceConfirmations: map[string]int{"0.0": 0, "1.0": -1}, }, triggerWithAllTopics: { - ChainSpecificName: triggerWithAllTopics, - ReadType: types.Event, - EventInputFields: []string{"Field1", "Field2", "Field3"}, + ChainSpecificName: triggerWithAllTopics, + ReadType: types.Event, + EventDefinitions: &types.EventDefinitions{ + InputFields: []string{"Field1", "Field2", "Field3"}, + PollingFilter: &types.PollingFilter{}, + }, ConfidenceConfirmations: map[string]int{"0.0": 0, "1.0": -1}, }, MethodReturningSeenStruct: { diff --git a/core/services/relay/evm/method_binding.go b/core/services/relay/evm/method_binding.go index 7484d17c3ef..3a212bfea4b 100644 --- a/core/services/relay/evm/method_binding.go +++ b/core/services/relay/evm/method_binding.go @@ -28,15 +28,17 @@ func (m *methodBinding) SetCodec(codec commontypes.RemoteCodec) { m.codec = codec } -func (m *methodBinding) Register(_ context.Context) error { +func (m *methodBinding) Bind(_ context.Context, binding commontypes.BoundContract) error { + m.address = common.HexToAddress(binding.Address) + m.bound = true return nil } -func (m *methodBinding) Unregister(_ context.Context) error { +func (m *methodBinding) Register(_ context.Context) error { return nil } -func (m *methodBinding) UnregisterAll(_ context.Context) error { +func (m *methodBinding) Unregister(_ context.Context) error { return nil } @@ -67,9 +69,3 @@ func (m *methodBinding) GetLatestValue(ctx context.Context, params, returnValue func (m *methodBinding) QueryKey(_ context.Context, _ query.KeyFilter, _ query.LimitAndSort, _ any) ([]commontypes.Sequence, error) { return nil, nil } - -func (m *methodBinding) Bind(_ context.Context, binding commontypes.BoundContract) error { - m.address = common.HexToAddress(binding.Address) - m.bound = true - return nil -} diff --git a/core/services/relay/evm/types/types.go b/core/services/relay/evm/types/types.go index a4c7e69a2fa..e29b1e6b77f 100644 --- a/core/services/relay/evm/types/types.go +++ b/core/services/relay/evm/types/types.go @@ -18,8 +18,10 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/codec" "github.com/smartcontractkit/chainlink-common/pkg/services" "github.com/smartcontractkit/chainlink-common/pkg/types" - + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller" + evmtypes "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/utils/big" + "github.com/smartcontractkit/chainlink/v2/core/store/models" ) type ChainWriterConfig struct { @@ -54,17 +56,59 @@ type CodecConfig struct { type ChainCodecConfig struct { TypeABI string `json:"typeAbi" toml:"typeABI"` - ModifierConfigs codec.ModifiersConfig `toml:"modifierConfigs,omitempty"` + ModifierConfigs codec.ModifiersConfig `json:"modifierConfigs,omitempty" toml:"modifierConfigs,omitempty"` +} + +type ContractPollingFilter struct { + GenericEventNames []string `json:"genericEventNames"` + PollingFilter `json:"pollingFilter"` +} + +type PollingFilter struct { + Topic2 evmtypes.HashArray `json:"topic2"` // list of possible values for topic2 + Topic3 evmtypes.HashArray `json:"topic3"` // list of possible values for topic3 + Topic4 evmtypes.HashArray `json:"topic4"` // list of possible values for topic4 + Retention models.Interval `json:"retention"` // maximum amount of time to retain logs + MaxLogsKept uint64 `json:"maxLogsKept"` // maximum number of logs to retain ( 0 = unlimited ) + LogsPerBlock uint64 `json:"logsPerBlock"` // rate limit ( maximum # of logs per block, 0 = unlimited ) +} + +func (f *PollingFilter) ToLPFilter(eventSigs evmtypes.HashArray) logpoller.Filter { + return logpoller.Filter{ + EventSigs: eventSigs, + Topic2: f.Topic2, + Topic3: f.Topic3, + Topic4: f.Topic4, + Retention: f.Retention.Duration(), + MaxLogsKept: f.MaxLogsKept, + LogsPerBlock: f.LogsPerBlock, + } } type ChainContractReader struct { - ContractABI string `json:"contractABI" toml:"contractABI"` + ContractABI string `json:"contractABI" toml:"contractABI"` + ContractPollingFilter `json:"contractPollingFilter,omitempty" toml:"contractPollingFilter,omitempty"` // key is genericName from config Configs map[string]*ChainReaderDefinition `json:"configs" toml:"configs"` } type ChainReaderDefinition chainReaderDefinitionFields +type EventDefinitions struct { + // GenericTopicNames helps QueryingKeys not rely on EVM specific topic names. Key is chain specific name, value is generic name. + // This helps us translate chain agnostic querying key "transfer-value" to EVM specific "evmTransferEvent-weiAmountTopic". + GenericTopicNames map[string]string `json:"genericTopicNames,omitempty"` + // key is a predefined generic name for evm log event data word + // for e.g. first evm data word(32bytes) of USDC log event is value so the key can be called value + GenericDataWordNames map[string]uint8 `json:"genericDataWordNames,omitempty"` + // InputFields allows you to choose which indexed fields are expected from the input + InputFields []string `json:"inputFields,omitempty"` + // PollingFilter should be defined on a contract level in ContractPollingFilter, + // unless event needs to override the contract level filter options. + // This will create a separate log poller filter for this event. + *PollingFilter `json:"pollingFilter,omitempty"` +} + // chainReaderDefinitionFields has the fields for ChainReaderDefinition but no methods. // This is necessary because package json recognizes the text encoding methods used for TOML, // and would infinitely recurse on itself. @@ -75,20 +119,16 @@ type chainReaderDefinitionFields struct { ReadType ReadType `json:"readType,omitempty"` InputModifications codec.ModifiersConfig `json:"inputModifications,omitempty"` OutputModifications codec.ModifiersConfig `json:"outputModifications,omitempty"` - - // EventInputFields allows you to choose which indexed fields are expected from the input - EventInputFields []string `json:"eventInputFields,omitempty"` - // GenericTopicNames helps QueryingKeys not rely on EVM specific topic names. Key is chain specific name, value is generic name. - // This helps us translate chain agnostic querying key "transfer-value" to EVM specific "evmTransferEvent-weiAmountTopic". - GenericTopicNames map[string]string `json:"genericTopicNames,omitempty"` - // key is a predefined generic name for evm log event data word - // for eg. first evm data word(32bytes) of USDC log event is value so the key can be called value - GenericDataWordNames map[string]uint8 `json:"genericDataWordNames,omitempty"` + EventDefinitions *EventDefinitions `json:"eventDefinitions,omitempty" toml:"eventDefinitions,omitempty"` // ConfidenceConfirmations is a mapping between a ConfidenceLevel and the confirmations associated. Confidence levels // should be valid float values. ConfidenceConfirmations map[string]int `json:"confidenceConfirmations,omitempty"` } +func (d *ChainReaderDefinition) HasPollingFilter() bool { + return d.EventDefinitions != nil && d.EventDefinitions.PollingFilter != nil +} + func (d *ChainReaderDefinition) MarshalText() ([]byte, error) { var b bytes.Buffer e := json.NewEncoder(&b) diff --git a/core/services/relay/evm/types/types_test.go b/core/services/relay/evm/types/types_test.go index fb394ddf38b..37d5e77693a 100644 --- a/core/services/relay/evm/types/types_test.go +++ b/core/services/relay/evm/types/types_test.go @@ -1,15 +1,21 @@ package types import ( + "encoding/json" "fmt" "testing" + "time" + "github.com/ethereum/go-ethereum/common" "github.com/pelletier/go-toml" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/smartcontractkit/chainlink-common/pkg/codec" + evmtypes "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/utils" "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" + "github.com/smartcontractkit/chainlink/v2/core/store/models" ) // ChainID *big.Big `json:"chainID"` @@ -39,3 +45,112 @@ FeedID = "0x%x" assert.Equal(t, fromBlock, rc.FromBlock) assert.Equal(t, feedID.Hex(), rc.FeedID.Hex()) } + +func Test_ChainReaderConfig(t *testing.T) { + tests := []struct { + name string + jsonInput string + expected ChainReaderConfig + }{ + { + name: "Valid JSON", + jsonInput: ` +{ + "contracts":{ + "Contract1":{ + "contractABI":"[ { \"anonymous\": false, \"inputs\": [ { \"indexed\": true, \"internalType\": \"address\", \"name\": \"requester\", \"type\": \"address\" }, { \"indexed\": false, \"internalType\": \"bytes32\", \"name\": \"configDigest\", \"type\": \"bytes32\" }, { \"indexed\": false, \"internalType\": \"uint32\", \"name\": \"epoch\", \"type\": \"uint32\" }, { \"indexed\": false, \"internalType\": \"uint8\", \"name\": \"round\", \"type\": \"uint8\" } ], \"name\": \"RoundRequested\", \"type\": \"event\" }, { \"inputs\": [], \"name\": \"latestTransmissionDetails\", \"outputs\": [ { \"internalType\": \"bytes32\", \"name\": \"configDigest\", \"type\": \"bytes32\" }, { \"internalType\": \"uint32\", \"name\": \"epoch\", \"type\": \"uint32\" }, { \"internalType\": \"uint8\", \"name\": \"round\", \"type\": \"uint8\" }, { \"internalType\": \"int192\", \"name\": \"latestAnswer_\", \"type\": \"int192\" }, { \"internalType\": \"uint64\", \"name\": \"latestTimestamp_\", \"type\": \"uint64\" } ], \"stateMutability\": \"view\", \"type\": \"function\" }]", + "contractPollingFilter":{ + "genericEventNames":[ + "event1", + "event2" + ], + "pollingFilter":{ + "topic2":[ + "0x1abbe4784b1fb071039bb9cb50b82978fb5d3ab98fb512c032e75786b93e2c52" + ], + "topic3":[ + "0x2abbe4784b1fb071039bb9cb50b82978fb5d3ab98fb512c032e75786b93e2c52" + ], + "topic4":[ + "0x3abbe4784b1fb071039bb9cb50b82978fb5d3ab98fb512c032e75786b93e2c52" + ], + "retention":"1m0s", + "maxLogsKept":100, + "logsPerBlock":10 + } + }, + "configs":{ + "config1":"{\"cacheEnabled\":true,\"chainSpecificName\":\"specificName1\",\"inputModifications\":[{\"Fields\":[\"ts\"],\"Type\":\"epoch to time\"},{\"Fields\":{\"a\":\"b\"},\"Type\":\"rename\"}],\"outputModifications\":[{\"Fields\":[\"ts\"],\"Type\":\"epoch to time\"},{\"Fields\":{\"c\":\"d\"},\"Type\":\"rename\"}],\"eventDefinitions\":{\"genericTopicNames\":{\"TopicKey1\":\"TopicVal1\"},\"genericDataWordNames\":{\"DataWordKey\":1},\"inputFields\":[\"Event1\",\"Event2\"],\"pollingFilter\":{\"topic2\":[\"0x4abbe4784b1fb071039bb9cb50b82978fb5d3ab98fb512c032e75786b93e2c52\"],\"topic3\":[\"0x5abbe4784b1fb071039bb9cb50b82978fb5d3ab98fb512c032e75786b93e2c52\"],\"topic4\":[\"0x6abbe4784b1fb071039bb9cb50b82978fb5d3ab98fb512c032e75786b93e2c52\"],\"retention\":\"1m0s\",\"maxLogsKept\":100,\"logsPerBlock\":10}},\"confidenceConfirmations\":{\"0.0\":0,\"1.0\":-1}}" + } + } + } +} +`, expected: ChainReaderConfig{ + Contracts: map[string]ChainContractReader{ + "Contract1": { + ContractABI: "[ { \"anonymous\": false, \"inputs\": [ { \"indexed\": true, \"internalType\": \"address\", \"name\": \"requester\", \"type\": \"address\" }, { \"indexed\": false, \"internalType\": \"bytes32\", \"name\": \"configDigest\", \"type\": \"bytes32\" }, { \"indexed\": false, \"internalType\": \"uint32\", \"name\": \"epoch\", \"type\": \"uint32\" }, { \"indexed\": false, \"internalType\": \"uint8\", \"name\": \"round\", \"type\": \"uint8\" } ], \"name\": \"RoundRequested\", \"type\": \"event\" }, { \"inputs\": [], \"name\": \"latestTransmissionDetails\", \"outputs\": [ { \"internalType\": \"bytes32\", \"name\": \"configDigest\", \"type\": \"bytes32\" }, { \"internalType\": \"uint32\", \"name\": \"epoch\", \"type\": \"uint32\" }, { \"internalType\": \"uint8\", \"name\": \"round\", \"type\": \"uint8\" }, { \"internalType\": \"int192\", \"name\": \"latestAnswer_\", \"type\": \"int192\" }, { \"internalType\": \"uint64\", \"name\": \"latestTimestamp_\", \"type\": \"uint64\" } ], \"stateMutability\": \"view\", \"type\": \"function\" }]", + ContractPollingFilter: ContractPollingFilter{ + GenericEventNames: []string{"event1", "event2"}, + PollingFilter: PollingFilter{ + Topic2: evmtypes.HashArray{common.HexToHash("0x1abbe4784b1fb071039bb9cb50b82978fb5d3ab98fb512c032e75786b93e2c52")}, + Topic3: evmtypes.HashArray{common.HexToHash("0x2abbe4784b1fb071039bb9cb50b82978fb5d3ab98fb512c032e75786b93e2c52")}, + Topic4: evmtypes.HashArray{common.HexToHash("0x3abbe4784b1fb071039bb9cb50b82978fb5d3ab98fb512c032e75786b93e2c52")}, + Retention: models.Interval(time.Minute * 1), + MaxLogsKept: 100, + LogsPerBlock: 10, + }, + }, + Configs: map[string]*ChainReaderDefinition{ + "config1": { + CacheEnabled: true, + ChainSpecificName: "specificName1", + ReadType: Method, + InputModifications: codec.ModifiersConfig{ + &codec.EpochToTimeModifierConfig{ + Fields: []string{"ts"}, + }, + &codec.RenameModifierConfig{ + Fields: map[string]string{ + "a": "b", + }, + }, + }, + OutputModifications: codec.ModifiersConfig{ + &codec.EpochToTimeModifierConfig{ + Fields: []string{"ts"}, + }, + &codec.RenameModifierConfig{ + Fields: map[string]string{ + "c": "d", + }, + }, + }, + ConfidenceConfirmations: map[string]int{"0.0": 0, "1.0": -1}, + EventDefinitions: &EventDefinitions{ + GenericTopicNames: map[string]string{"TopicKey1": "TopicVal1"}, + GenericDataWordNames: map[string]uint8{"DataWordKey": 1}, + InputFields: []string{"Event1", "Event2"}, + PollingFilter: &PollingFilter{ + Topic2: evmtypes.HashArray{common.HexToHash("0x4abbe4784b1fb071039bb9cb50b82978fb5d3ab98fb512c032e75786b93e2c52")}, + Topic3: evmtypes.HashArray{common.HexToHash("0x5abbe4784b1fb071039bb9cb50b82978fb5d3ab98fb512c032e75786b93e2c52")}, + Topic4: evmtypes.HashArray{common.HexToHash("0x6abbe4784b1fb071039bb9cb50b82978fb5d3ab98fb512c032e75786b93e2c52")}, + Retention: models.Interval(time.Minute * 1), + MaxLogsKept: 100, + LogsPerBlock: 10, + }, + }, + }, + }, + }, + }, + }, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var config ChainReaderConfig + config.Contracts = make(map[string]ChainContractReader) + require.Nil(t, json.Unmarshal([]byte(tt.jsonInput), &config)) + require.Equal(t, tt.expected, config) + }) + } +} diff --git a/integration-tests/actions/ocr2_helpers_local.go b/integration-tests/actions/ocr2_helpers_local.go index 8a0a02c050f..733b6903552 100644 --- a/integration-tests/actions/ocr2_helpers_local.go +++ b/integration-tests/actions/ocr2_helpers_local.go @@ -132,6 +132,9 @@ func CreateOCRv2JobsLocal( ocrSpec.OCR2OracleSpec.RelayConfig["chainReader"] = evmtypes.ChainReaderConfig{ Contracts: map[string]evmtypes.ChainContractReader{ "median": { + ContractPollingFilter: evmtypes.ContractPollingFilter{ + GenericEventNames: []string{"LatestRoundRequested"}, + }, ContractABI: `[{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"requester","type":"address"},{"indexed":false,"internalType":"bytes32","name":"configDigest","type":"bytes32"},{"indexed":false,"internalType":"uint32","name":"epoch","type":"uint32"},{"indexed":false,"internalType":"uint8","name":"round","type":"uint8"}],"name":"RoundRequested","type":"event"},{"inputs":[],"name":"latestTransmissionDetails","outputs":[{"internalType":"bytes32","name":"configDigest","type":"bytes32"},{"internalType":"uint32","name":"epoch","type":"uint32"},{"internalType":"uint8","name":"round","type":"uint8"},{"internalType":"int192","name":"latestAnswer_","type":"int192"},{"internalType":"uint64","name":"latestTimestamp_","type":"uint64"}],"stateMutability":"view","type":"function"}]`, Configs: map[string]*evmtypes.ChainReaderDefinition{ "LatestTransmissionDetails": {