diff --git a/core/capabilities/triggers/logevent/logeventcap/event_trigger-schema.json b/core/capabilities/triggers/logevent/logeventcap/event_trigger-schema.json new file mode 100644 index 00000000000..a60d8823582 --- /dev/null +++ b/core/capabilities/triggers/logevent/logeventcap/event_trigger-schema.json @@ -0,0 +1,75 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/smartcontractkit/chainlink/v2/core/capabilities/triggers/logevent/logeventcap/log-event-trigger", + "$defs": { + "head": { + "type": "object", + "properties": { + "Height": { + "type": "string", + "minLength": 1 + }, + "Hash": { + "type": "string", + "minLength": 1 + }, + "Timestamp": { + "type": "integer", + "minimum": 0 + } + } + }, + "config": { + "type": "object", + "properties": { + "contractName": { + "type": "string", + "minLength": 1 + }, + "contractAddress": { + "type": "string", + "minLength": 1 + }, + "contractEventName": { + "type": "string", + "minLength": 1 + }, + "contractReaderConfig": { + "type": "object", + "properties": { + "contracts": { + "type": "object" + } + }, + "required": ["contracts"] + } + }, + "required": ["contractName", "contractAddress", "contractEventName", "contractReaderConfig"] + }, + "output": { + "type": "object", + "properties": { + "Cursor": { + "type": "string", + "minLength": 1 + }, + "Head": { + "$ref": "#/$defs/head" + }, + "Data": { + "type": "object" + } + }, + "required": ["Cursor", "Head", "Data"] + } + }, + "type": "object", + "properties": { + "Config": { + "$ref": "#/$defs/config" + }, + "Outputs": { + "$ref": "#/$defs/output" + } + } + } \ No newline at end of file diff --git a/core/capabilities/triggers/logevent/logeventcap/event_trigger_generated.go b/core/capabilities/triggers/logevent/logeventcap/event_trigger_generated.go new file mode 100644 index 00000000000..23376958309 --- /dev/null +++ b/core/capabilities/triggers/logevent/logeventcap/event_trigger_generated.go @@ -0,0 +1,160 @@ +// Code generated by github.com/smartcontractkit/chainlink-common/pkg/capabilities/cli, DO NOT EDIT. + +package logeventcap + +import ( + "encoding/json" + "fmt" +) + +type Config struct { + // ContractAddress corresponds to the JSON schema field "contractAddress". + ContractAddress string `json:"contractAddress" yaml:"contractAddress" mapstructure:"contractAddress"` + + // ContractEventName corresponds to the JSON schema field "contractEventName". + ContractEventName string `json:"contractEventName" yaml:"contractEventName" mapstructure:"contractEventName"` + + // ContractName corresponds to the JSON schema field "contractName". + ContractName string `json:"contractName" yaml:"contractName" mapstructure:"contractName"` + + // ContractReaderConfig corresponds to the JSON schema field + // "contractReaderConfig". + ContractReaderConfig ConfigContractReaderConfig `json:"contractReaderConfig" yaml:"contractReaderConfig" mapstructure:"contractReaderConfig"` +} + +type ConfigContractReaderConfig struct { + // Contracts corresponds to the JSON schema field "contracts". + Contracts ConfigContractReaderConfigContracts `json:"contracts" yaml:"contracts" mapstructure:"contracts"` +} + +type ConfigContractReaderConfigContracts map[string]interface{} + +// UnmarshalJSON implements json.Unmarshaler. +func (j *ConfigContractReaderConfig) UnmarshalJSON(b []byte) error { + var raw map[string]interface{} + if err := json.Unmarshal(b, &raw); err != nil { + return err + } + if _, ok := raw["contracts"]; raw != nil && !ok { + return fmt.Errorf("field contracts in ConfigContractReaderConfig: required") + } + type Plain ConfigContractReaderConfig + var plain Plain + if err := json.Unmarshal(b, &plain); err != nil { + return err + } + *j = ConfigContractReaderConfig(plain) + return nil +} + +// UnmarshalJSON implements json.Unmarshaler. +func (j *Config) UnmarshalJSON(b []byte) error { + var raw map[string]interface{} + if err := json.Unmarshal(b, &raw); err != nil { + return err + } + if _, ok := raw["contractAddress"]; raw != nil && !ok { + return fmt.Errorf("field contractAddress in Config: required") + } + if _, ok := raw["contractEventName"]; raw != nil && !ok { + return fmt.Errorf("field contractEventName in Config: required") + } + if _, ok := raw["contractName"]; raw != nil && !ok { + return fmt.Errorf("field contractName in Config: required") + } + if _, ok := raw["contractReaderConfig"]; raw != nil && !ok { + return fmt.Errorf("field contractReaderConfig in Config: required") + } + type Plain Config + var plain Plain + if err := json.Unmarshal(b, &plain); err != nil { + return err + } + if len(plain.ContractAddress) < 1 { + return fmt.Errorf("field %s length: must be >= %d", "contractAddress", 1) + } + if len(plain.ContractEventName) < 1 { + return fmt.Errorf("field %s length: must be >= %d", "contractEventName", 1) + } + if len(plain.ContractName) < 1 { + return fmt.Errorf("field %s length: must be >= %d", "contractName", 1) + } + *j = Config(plain) + return nil +} + +type Head struct { + // Hash corresponds to the JSON schema field "Hash". + Hash *string `json:"Hash,omitempty" yaml:"Hash,omitempty" mapstructure:"Hash,omitempty"` + + // Height corresponds to the JSON schema field "Height". + Height *string `json:"Height,omitempty" yaml:"Height,omitempty" mapstructure:"Height,omitempty"` + + // Timestamp corresponds to the JSON schema field "Timestamp". + Timestamp *uint64 `json:"Timestamp,omitempty" yaml:"Timestamp,omitempty" mapstructure:"Timestamp,omitempty"` +} + +// UnmarshalJSON implements json.Unmarshaler. +func (j *Head) UnmarshalJSON(b []byte) error { + type Plain Head + var plain Plain + if err := json.Unmarshal(b, &plain); err != nil { + return err + } + if plain.Hash != nil && len(*plain.Hash) < 1 { + return fmt.Errorf("field %s length: must be >= %d", "Hash", 1) + } + if plain.Height != nil && len(*plain.Height) < 1 { + return fmt.Errorf("field %s length: must be >= %d", "Height", 1) + } + *j = Head(plain) + return nil +} + +type Output struct { + // Cursor corresponds to the JSON schema field "Cursor". + Cursor string `json:"Cursor" yaml:"Cursor" mapstructure:"Cursor"` + + // Data corresponds to the JSON schema field "Data". + Data OutputData `json:"Data" yaml:"Data" mapstructure:"Data"` + + // Head corresponds to the JSON schema field "Head". + Head Head `json:"Head" yaml:"Head" mapstructure:"Head"` +} + +type OutputData map[string]interface{} + +// UnmarshalJSON implements json.Unmarshaler. +func (j *Output) UnmarshalJSON(b []byte) error { + var raw map[string]interface{} + if err := json.Unmarshal(b, &raw); err != nil { + return err + } + if _, ok := raw["Cursor"]; raw != nil && !ok { + return fmt.Errorf("field Cursor in Output: required") + } + if _, ok := raw["Data"]; raw != nil && !ok { + return fmt.Errorf("field Data in Output: required") + } + if _, ok := raw["Head"]; raw != nil && !ok { + return fmt.Errorf("field Head in Output: required") + } + type Plain Output + var plain Plain + if err := json.Unmarshal(b, &plain); err != nil { + return err + } + if len(plain.Cursor) < 1 { + return fmt.Errorf("field %s length: must be >= %d", "Cursor", 1) + } + *j = Output(plain) + return nil +} + +type Trigger struct { + // Config corresponds to the JSON schema field "Config". + Config *Config `json:"Config,omitempty" yaml:"Config,omitempty" mapstructure:"Config,omitempty"` + + // Outputs corresponds to the JSON schema field "Outputs". + Outputs *Output `json:"Outputs,omitempty" yaml:"Outputs,omitempty" mapstructure:"Outputs,omitempty"` +} diff --git a/core/capabilities/triggers/logevent/logeventcap/gen.go b/core/capabilities/triggers/logevent/logeventcap/gen.go new file mode 100644 index 00000000000..09655a9a113 --- /dev/null +++ b/core/capabilities/triggers/logevent/logeventcap/gen.go @@ -0,0 +1,5 @@ +package logeventcap + +import _ "github.com/smartcontractkit/chainlink-common/pkg/capabilities/cli/cmd" // Required so that the tool is available to be run in go generate below. + +//go:generate go run github.com/smartcontractkit/chainlink-common/pkg/capabilities/cli/cmd/generate-types --dir $GOFILE diff --git a/core/capabilities/triggers/logevent/logeventcap/logeventcaptest/trigger_mock_generated.go b/core/capabilities/triggers/logevent/logeventcap/logeventcaptest/trigger_mock_generated.go new file mode 100644 index 00000000000..f91dbc6e2b3 --- /dev/null +++ b/core/capabilities/triggers/logevent/logeventcap/logeventcaptest/trigger_mock_generated.go @@ -0,0 +1,17 @@ +// Code generated by github.com/smartcontractkit/chainlink-common/pkg/capabilities/cli, DO NOT EDIT. + +// Code generated by github.com/smartcontractkit/chainlink-common/pkg/capabilities/cli, DO NOT EDIT. + +package logeventcaptest + +import ( + "github.com/smartcontractkit/chainlink-common/pkg/workflows/sdk/testutils" + "github.com/smartcontractkit/chainlink/v2/core/capabilities/triggers/logevent/logeventcap" +) + +// Trigger registers a new capability mock with the runner +func Trigger(runner *testutils.Runner, id string, fn func() (logeventcap.Output, error)) *testutils.TriggerMock[logeventcap.Output] { + mock := testutils.MockTrigger[logeventcap.Output](id, fn) + runner.MockCapability(id, nil, mock) + return mock +} diff --git a/core/capabilities/triggers/logevent/logeventcap/trigger_builders_generated.go b/core/capabilities/triggers/logevent/logeventcap/trigger_builders_generated.go new file mode 100644 index 00000000000..8788f005d63 --- /dev/null +++ b/core/capabilities/triggers/logevent/logeventcap/trigger_builders_generated.go @@ -0,0 +1,156 @@ +// Code generated by github.com/smartcontractkit/chainlink-common/pkg/capabilities/cli, DO NOT EDIT. + +package logeventcap + +import ( + "github.com/smartcontractkit/chainlink-common/pkg/capabilities" + "github.com/smartcontractkit/chainlink-common/pkg/workflows/sdk" +) + +func (cfg Config) New(w *sdk.WorkflowSpecFactory, id string) OutputCap { + ref := "trigger" + def := sdk.StepDefinition{ + ID: id, Ref: ref, + Inputs: sdk.StepInputs{}, + Config: map[string]any{ + "contractAddress": cfg.ContractAddress, + "contractEventName": cfg.ContractEventName, + "contractName": cfg.ContractName, + "contractReaderConfig": cfg.ContractReaderConfig, + }, + CapabilityType: capabilities.CapabilityTypeTrigger, + } + + step := sdk.Step[Output]{Definition: def} + return OutputCapFromStep(w, step) +} + +type HeadCap interface { + sdk.CapDefinition[Head] + Hash() sdk.CapDefinition[string] + Height() sdk.CapDefinition[string] + Timestamp() sdk.CapDefinition[uint64] + private() +} + +// HeadCapFromStep should only be called from generated code to assure type safety +func HeadCapFromStep(w *sdk.WorkflowSpecFactory, step sdk.Step[Head]) HeadCap { + raw := step.AddTo(w) + return &head{CapDefinition: raw} +} + +type head struct { + sdk.CapDefinition[Head] +} + +func (*head) private() {} +func (c *head) Hash() sdk.CapDefinition[string] { + return sdk.AccessField[Head, string](c.CapDefinition, "Hash") +} +func (c *head) Height() sdk.CapDefinition[string] { + return sdk.AccessField[Head, string](c.CapDefinition, "Height") +} +func (c *head) Timestamp() sdk.CapDefinition[uint64] { + return sdk.AccessField[Head, uint64](c.CapDefinition, "Timestamp") +} + +func NewHeadFromFields( + hash sdk.CapDefinition[string], + height sdk.CapDefinition[string], + timestamp sdk.CapDefinition[uint64]) HeadCap { + return &simpleHead{ + CapDefinition: sdk.ComponentCapDefinition[Head]{ + "Hash": hash.Ref(), + "Height": height.Ref(), + "Timestamp": timestamp.Ref(), + }, + hash: hash, + height: height, + timestamp: timestamp, + } +} + +type simpleHead struct { + sdk.CapDefinition[Head] + hash sdk.CapDefinition[string] + height sdk.CapDefinition[string] + timestamp sdk.CapDefinition[uint64] +} + +func (c *simpleHead) Hash() sdk.CapDefinition[string] { + return c.hash +} +func (c *simpleHead) Height() sdk.CapDefinition[string] { + return c.height +} +func (c *simpleHead) Timestamp() sdk.CapDefinition[uint64] { + return c.timestamp +} + +func (c *simpleHead) private() {} + +type OutputCap interface { + sdk.CapDefinition[Output] + Cursor() sdk.CapDefinition[string] + Data() OutputDataCap + Head() HeadCap + private() +} + +// OutputCapFromStep should only be called from generated code to assure type safety +func OutputCapFromStep(w *sdk.WorkflowSpecFactory, step sdk.Step[Output]) OutputCap { + raw := step.AddTo(w) + return &output{CapDefinition: raw} +} + +type output struct { + sdk.CapDefinition[Output] +} + +func (*output) private() {} +func (c *output) Cursor() sdk.CapDefinition[string] { + return sdk.AccessField[Output, string](c.CapDefinition, "Cursor") +} +func (c *output) Data() OutputDataCap { + return OutputDataCap(sdk.AccessField[Output, OutputData](c.CapDefinition, "Data")) +} +func (c *output) Head() HeadCap { + return &head{CapDefinition: sdk.AccessField[Output, Head](c.CapDefinition, "Head")} +} + +func NewOutputFromFields( + cursor sdk.CapDefinition[string], + data OutputDataCap, + head HeadCap) OutputCap { + return &simpleOutput{ + CapDefinition: sdk.ComponentCapDefinition[Output]{ + "Cursor": cursor.Ref(), + "Data": data.Ref(), + "Head": head.Ref(), + }, + cursor: cursor, + data: data, + head: head, + } +} + +type simpleOutput struct { + sdk.CapDefinition[Output] + cursor sdk.CapDefinition[string] + data OutputDataCap + head HeadCap +} + +func (c *simpleOutput) Cursor() sdk.CapDefinition[string] { + return c.cursor +} +func (c *simpleOutput) Data() OutputDataCap { + return c.data +} +func (c *simpleOutput) Head() HeadCap { + return c.head +} + +func (c *simpleOutput) private() {} + +type OutputDataCap sdk.CapDefinition[OutputData] diff --git a/core/capabilities/triggers/logevent/service.go b/core/capabilities/triggers/logevent/service.go index 7ed4855e097..52e56d991f9 100644 --- a/core/capabilities/triggers/logevent/service.go +++ b/core/capabilities/triggers/logevent/service.go @@ -9,6 +9,8 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink-common/pkg/services" "github.com/smartcontractkit/chainlink-common/pkg/types/core" + + "github.com/smartcontractkit/chainlink/v2/core/capabilities/triggers/logevent/logeventcap" ) const ID = "log-event-trigger-%s-%s@1.0.0" @@ -24,7 +26,7 @@ type Input struct { type TriggerService struct { services.StateMachine capabilities.CapabilityInfo - capabilities.Validator[RequestConfig, Input, capabilities.TriggerResponse] + capabilities.Validator[logeventcap.Config, Input, capabilities.TriggerResponse] lggr logger.Logger triggers CapabilitiesStore[logEventTrigger, capabilities.TriggerResponse] relayer core.Relayer @@ -69,7 +71,7 @@ func NewTriggerService(ctx context.Context, if err != nil { return s, err } - s.Validator = capabilities.NewValidator[RequestConfig, Input, capabilities.TriggerResponse](capabilities.ValidatorArgs{Info: s.CapabilityInfo}) + s.Validator = capabilities.NewValidator[logeventcap.Config, Input, capabilities.TriggerResponse](capabilities.ValidatorArgs{Info: s.CapabilityInfo}) return s, nil } @@ -83,7 +85,8 @@ func (s *TriggerService) Info(ctx context.Context) (capabilities.CapabilityInfo, // Register a new trigger // Can register triggers before the service is actively scheduling -func (s *TriggerService) RegisterTrigger(ctx context.Context, req capabilities.TriggerRegistrationRequest) (<-chan capabilities.TriggerResponse, error) { +func (s *TriggerService) RegisterTrigger(ctx context.Context, + req capabilities.TriggerRegistrationRequest) (<-chan capabilities.TriggerResponse, error) { if req.Config == nil { return nil, errors.New("config is required to register a log event trigger") } @@ -104,7 +107,7 @@ func (s *TriggerService) RegisterTrigger(ctx context.Context, req capabilities.T }) }) if !ok { - return nil, fmt.Errorf("cannot create new trigger since LogEventTriggerService has been stopped") + return nil, fmt.Errorf("cannot create new trigger since LogEventTriggerCapabilityService has been stopped") } if err != nil { return nil, fmt.Errorf("create new trigger failed %w", err) diff --git a/core/capabilities/triggers/logevent/trigger.go b/core/capabilities/triggers/logevent/trigger.go index 9a0e1d036c7..379d9483c24 100644 --- a/core/capabilities/triggers/logevent/trigger.go +++ b/core/capabilities/triggers/logevent/trigger.go @@ -15,17 +15,9 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/types/query" "github.com/smartcontractkit/chainlink-common/pkg/types/query/primitives" "github.com/smartcontractkit/chainlink-common/pkg/values" -) -// Log Event Trigger Capability Request Config Details -type RequestConfig struct { - ContractName string `json:"contractName"` - ContractAddress string `json:"contractAddress"` - ContractEventName string `json:"contractEventName"` - // Log Event Trigger capability takes in a []byte as ContractReaderConfig - // to not depend on evm ChainReaderConfig type and be chain agnostic - ContractReaderConfig map[string]any `json:"contractReaderConfig"` -} + "github.com/smartcontractkit/chainlink/v2/core/capabilities/triggers/logevent/logeventcap" +) // LogEventTrigger struct to listen for Contract events using ContractReader gRPC client // in a loop with a periodic delay of pollPeriod milliseconds, which is specified in @@ -35,7 +27,7 @@ type logEventTrigger struct { lggr logger.Logger // Contract address and Event Signature to monitor for - reqConfig *RequestConfig + reqConfig *logeventcap.Config contractReader types.ContractReader relayer core.Relayer startBlockNum uint64 @@ -51,7 +43,7 @@ type logEventTrigger struct { func newLogEventTrigger(ctx context.Context, lggr logger.Logger, workflowID string, - reqConfig *RequestConfig, + reqConfig *logeventcap.Config, logEventConfig Config, relayer core.Relayer) (*logEventTrigger, chan capabilities.TriggerResponse, error) { jsonBytes, err := json.Marshal(reqConfig.ContractReaderConfig) @@ -124,7 +116,7 @@ func (l *logEventTrigger) listen() { // Listen for events from lookbackPeriod var logs []types.Sequence var err error - logData := make(map[string]any) + var logData values.Value cursor := "" limitAndSort := query.LimitAndSort{ SortBy: []query.SortBy{query.NewSortByTimestamp(query.Asc)}, diff --git a/core/services/relay/evm/capabilities/log_event_trigger_test.go b/core/services/relay/evm/capabilities/log_event_trigger_test.go index f2104529b7f..e196ae5bf80 100644 --- a/core/services/relay/evm/capabilities/log_event_trigger_test.go +++ b/core/services/relay/evm/capabilities/log_event_trigger_test.go @@ -9,6 +9,7 @@ import ( "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" + "github.com/smartcontractkit/chainlink-common/pkg/capabilities" "github.com/smartcontractkit/chainlink-common/pkg/services/servicetest" commontypes "github.com/smartcontractkit/chainlink-common/pkg/types" commonmocks "github.com/smartcontractkit/chainlink-common/pkg/types/core/mocks" @@ -19,6 +20,7 @@ import ( // Test for Log Event Trigger Capability happy path for EVM func TestLogEventTriggerEVMHappyPath(t *testing.T) { + t.Parallel() th := testutils.NewContractReaderTH(t) logEventConfig := logevent.Config{ @@ -59,31 +61,97 @@ func TestLogEventTriggerEVMHappyPath(t *testing.T) { log1Ch, err := logEventTriggerService.RegisterTrigger(ctx, th.LogEmitterRegRequest) require.NoError(t, err) - expectedLogVal := int64(10) + emitLogTxnAndWaitForLog(t, th, log1Ch, []*big.Int{big.NewInt(10)}) +} + +// Test if Log Event Trigger Capability is able to receive only new logs +// by using cursor and does not receive duplicate logs +func TestLogEventTriggerCursorNewLogs(t *testing.T) { + t.Parallel() + th := testutils.NewContractReaderTH(t) + + logEventConfig := logevent.Config{ + ChainID: th.BackendTH.ChainID.String(), + Network: "evm", + LookbackBlocks: 1000, + PollPeriod: 1000, + } + + // Create a new contract reader to return from mock relayer + ctx := coretestutils.Context(t) + + // Fetch latest head from simulated backend to return from mock relayer + height, err := th.BackendTH.EVMClient.LatestBlockHeight(ctx) + require.NoError(t, err) + block, err := th.BackendTH.EVMClient.BlockByNumber(ctx, height) + require.NoError(t, err) + + // Mock relayer to return a New ContractReader instead of gRPC client of a ContractReader + relayer := commonmocks.NewRelayer(t) + relayer.On("NewContractReader", mock.Anything, th.LogEmitterContractReaderCfg).Return(th.LogEmitterContractReader, nil).Once() + relayer.On("LatestHead", mock.Anything).Return(commontypes.Head{ + Height: height.String(), + Hash: block.Hash().Bytes(), + Timestamp: block.Time(), + }, nil).Once() + + // Create Log Event Trigger Service and register trigger + logEventTriggerService, err := logevent.NewTriggerService(ctx, + th.BackendTH.Lggr, + relayer, + logEventConfig) + require.NoError(t, err) + + // Start the service + servicetest.Run(t, logEventTriggerService) + + log1Ch, err := logEventTriggerService.RegisterTrigger(ctx, th.LogEmitterRegRequest) + require.NoError(t, err) - // Send a blockchain transaction that emits logs + emitLogTxnAndWaitForLog(t, th, log1Ch, []*big.Int{big.NewInt(10)}) + + // This confirms that the cursor being tracked by log event trigger capability + // works correctly and does not send old logs again as duplicates to the + // callback channel log1Ch + emitLogTxnAndWaitForLog(t, th, log1Ch, []*big.Int{big.NewInt(11), big.NewInt(12)}) +} + +// Send a transaction to EmitLog contract to emit Log1 events with given +// input parameters and wait for those logs to be received from relayer +// and ContractReader's QueryKey APIs used by Log Event Trigger +func emitLogTxnAndWaitForLog(t *testing.T, + th *testutils.ContractReaderTH, + log1Ch <-chan capabilities.TriggerResponse, + expectedLogVals []*big.Int) { done := make(chan struct{}) - t.Cleanup(func() { <-done }) + var err error go func() { defer close(done) _, err = - th.LogEmitterContract.EmitLog1(th.BackendTH.ContractsOwner, []*big.Int{big.NewInt(expectedLogVal)}) + th.LogEmitterContract.EmitLog1(th.BackendTH.ContractsOwner, expectedLogVals) assert.NoError(t, err) th.BackendTH.Backend.Commit() th.BackendTH.Backend.Commit() th.BackendTH.Backend.Commit() }() - // Wait for logs with a timeout - _, output, err := testutils.WaitForLog(th.BackendTH.Lggr, log1Ch, 15*time.Second) - require.NoError(t, err) - th.BackendTH.Lggr.Infow("EmitLog", "output", output) - // Verify if valid cursor is returned - cursor, err := testutils.GetStrVal(output, "Cursor") - require.NoError(t, err) - require.True(t, len(cursor) > 60) - // Verify if Arg0 is correct - actualLogVal, err := testutils.GetBigIntValL2(output, "Data", "Arg0") - require.NoError(t, err) - require.Equal(t, expectedLogVal, actualLogVal.Int64()) + for _, expectedLogVal := range expectedLogVals { + // Wait for logs with a timeout + _, output, err := testutils.WaitForLog(th.BackendTH.Lggr, log1Ch, 15*time.Second) + require.NoError(t, err) + th.BackendTH.Lggr.Infow("EmitLog", "output", output) + + // Verify if valid cursor is returned + cursor, err := testutils.GetStrVal(output, "Cursor") + require.NoError(t, err) + require.True(t, len(cursor) > 60) + + // Verify if Arg0 is correct + actualLogVal, err := testutils.GetBigIntValL2(output, "Data", "Arg0") + require.NoError(t, err) + + require.Equal(t, expectedLogVal.Int64(), actualLogVal.Int64()) + } + + <-done } diff --git a/core/services/relay/evm/capabilities/testutils/chain_reader.go b/core/services/relay/evm/capabilities/testutils/chain_reader.go index 3f0bf82da81..57dc21c426d 100644 --- a/core/services/relay/evm/capabilities/testutils/chain_reader.go +++ b/core/services/relay/evm/capabilities/testutils/chain_reader.go @@ -13,7 +13,7 @@ import ( commoncaps "github.com/smartcontractkit/chainlink-common/pkg/capabilities" commontypes "github.com/smartcontractkit/chainlink-common/pkg/types" commonvalues "github.com/smartcontractkit/chainlink-common/pkg/values" - "github.com/smartcontractkit/chainlink/v2/core/capabilities/triggers/logevent" + "github.com/smartcontractkit/chainlink/v2/core/capabilities/triggers/logevent/logeventcap" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/generated/log_emitter" coretestutils "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" "github.com/smartcontractkit/chainlink/v2/core/logger" @@ -44,7 +44,7 @@ func NewContractReaderTH(t *testing.T) *ContractReaderTH { require.NoError(t, err) // Create new contract reader - reqConfig := logevent.RequestConfig{ + reqConfig := logeventcap.Config{ ContractName: "LogEmitter", ContractAddress: logEmitterAddress.Hex(), ContractEventName: "Log1", @@ -72,7 +72,7 @@ func NewContractReaderTH(t *testing.T) *ContractReaderTH { // and be chain agnostic contractReaderCfgBytes, err := json.Marshal(contractReaderCfg) require.NoError(t, err) - contractReaderCfgMap := make(map[string]any) + var contractReaderCfgMap logeventcap.ConfigContractReaderConfig err = json.Unmarshal(contractReaderCfgBytes, &contractReaderCfgMap) require.NoError(t, err) // Encode the config map as JSON to specify in the expected call in mocked object diff --git a/go.mod b/go.mod index bc65ed6296f..c192323e61d 100644 --- a/go.mod +++ b/go.mod @@ -144,6 +144,7 @@ require ( github.com/Microsoft/go-winio v0.6.1 // indirect github.com/VictoriaMetrics/fastcache v1.12.1 // indirect github.com/armon/go-metrics v0.4.1 // indirect + github.com/atombender/go-jsonschema v0.16.1-0.20240916205339-a74cd4e2851c // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/benbjohnson/clock v1.3.5 // indirect github.com/beorn7/perks v1.0.1 // indirect @@ -211,6 +212,7 @@ require ( github.com/go-playground/validator/v10 v10.22.0 // indirect github.com/go-webauthn/x v0.1.5 // indirect github.com/goccy/go-json v0.10.2 // indirect + github.com/goccy/go-yaml v1.12.0 // indirect github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect github.com/gofrs/flock v0.8.1 // indirect github.com/gogo/protobuf v1.3.3 // indirect @@ -245,6 +247,7 @@ require ( github.com/huandu/skiplist v1.2.0 // indirect github.com/huandu/xstrings v1.4.0 // indirect github.com/huin/goupnp v1.3.0 // indirect + github.com/iancoleman/strcase v0.3.0 // indirect github.com/imdario/mergo v0.3.16 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/invopop/jsonschema v0.12.0 // indirect @@ -273,6 +276,7 @@ require ( github.com/mimoo/StrobeGo v0.0.0-20210601165009-122bf33a46e0 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-testing-interface v1.14.1 // indirect + github.com/mitchellh/go-wordwrap v1.0.1 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/mmcloughlin/addchain v0.4.0 // indirect @@ -294,6 +298,7 @@ require ( github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/sanity-io/litter v1.5.5 // indirect github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 // indirect github.com/sasha-s/go-deadlock v0.3.1 // indirect github.com/sethvargo/go-retry v0.2.4 // indirect @@ -348,6 +353,7 @@ require ( go.uber.org/ratelimit v0.3.0 // indirect golang.org/x/arch v0.8.0 // indirect golang.org/x/sys v0.25.0 // indirect + golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect google.golang.org/api v0.188.0 // indirect google.golang.org/genproto v0.0.0-20240711142825-46eb208f015d // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240822170219-fc7c04adadcd // indirect @@ -361,12 +367,7 @@ require ( sigs.k8s.io/yaml v1.4.0 // indirect ) -replace ( - // until merged upstream: https://github.com/omissis/go-jsonschema/pull/264 - github.com/atombender/go-jsonschema => github.com/nolag/go-jsonschema v0.16.0-rtinianov - - // replicating the replace directive on cosmos SDK - github.com/gogo/protobuf => github.com/regen-network/protobuf v1.3.3-alpha.regen.1 -) +// replicating the replace directive on cosmos SDK +replace github.com/gogo/protobuf => github.com/regen-network/protobuf v1.3.3-alpha.regen.1 replace github.com/sourcegraph/sourcegraph/lib => github.com/sourcegraph/sourcegraph-public-snapshot/lib v0.0.0-20240822153003-c864f15af264 diff --git a/go.sum b/go.sum index c528f8999cd..89a1b45d722 100644 --- a/go.sum +++ b/go.sum @@ -127,6 +127,8 @@ github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmV github.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJA= github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/atombender/go-jsonschema v0.16.1-0.20240916205339-a74cd4e2851c h1:cxQVoh6kY+c4b0HUchHjGWBI8288VhH50qxKG3hdEg0= +github.com/atombender/go-jsonschema v0.16.1-0.20240916205339-a74cd4e2851c/go.mod h1:3XzxudkrYVUvbduN/uI2fl4lSrMSzU0+3RCu2mpnfx8= github.com/avast/retry-go/v4 v4.6.0 h1:K9xNA+KeB8HHc2aWFuLb25Offp+0iVRXEvFx8IinRJA= github.com/avast/retry-go/v4 v4.6.0/go.mod h1:gvWlPhBVsvBbLkVGDg/KwvBv0bEkCOLRRSHKIr2PyOE= github.com/aws/aws-sdk-go v1.22.1/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= @@ -277,6 +279,7 @@ github.com/danieljoos/wincred v1.1.2 h1:QLdCxFs1/Yl4zduvBdcHB8goaYk9RARS2SgLLRuA github.com/danieljoos/wincred v1.1.2/go.mod h1:GijpziifJoIBfYh+S7BbkdUTU4LfM+QnGqR5Vl2tAx0= github.com/danielkov/gin-helmet v0.0.0-20171108135313-1387e224435e h1:5jVSh2l/ho6ajWhSPNN84eHEdq3dp0T7+f6r3Tc6hsk= github.com/danielkov/gin-helmet v0.0.0-20171108135313-1387e224435e/go.mod h1:IJgIiGUARc4aOr4bOQ85klmjsShkEEfiRc6q/yBSfo8= +github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -445,6 +448,8 @@ github.com/go-webauthn/x v0.1.5/go.mod h1:qbzWwcFcv4rTwtCLOZd+icnr6B7oSsAGZJqlt8 github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/goccy/go-yaml v1.12.0 h1:/1WHjnMsI1dlIBQutrvSMGZRQufVO3asrHfTwfACoPM= +github.com/goccy/go-yaml v1.12.0/go.mod h1:wKnAMd44+9JAAnGQpWVEgBzGt3YuTaQ4uXoHvE4m7WU= github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 h1:ZpnhV/YsD2/4cESfV5+Hoeu/iUR3ruzNvZ+yQfO03a0= github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= @@ -664,6 +669,8 @@ github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc= github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= +github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI= +github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= @@ -847,6 +854,8 @@ github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrk github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= +github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= +github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= @@ -938,6 +947,7 @@ github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -1011,6 +1021,8 @@ github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6ke github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/sanity-io/litter v1.5.5 h1:iE+sBxPBzoK6uaEP5Lt3fHNgpKcHXc/A2HGETy0uJQo= +github.com/sanity-io/litter v1.5.5/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U= github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4= github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY= github.com/sasha-s/go-deadlock v0.3.1 h1:sqv7fDNShgjcaxkO0JNcOAlr8B9+cV5Ey/OB71efZx0= @@ -1105,6 +1117,7 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v0.0.0-20161117074351-18a02ba4a312/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -1581,6 +1594,8 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY= +golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= gonum.org/v1/gonum v0.15.0 h1:2lYxjRbTYyxkJxlhC+LvJIx3SsANPdRybu1tGj9/OrQ= gonum.org/v1/gonum v0.15.0/go.mod h1:xzZVBJBtS+Mz4q0Yl2LJTk+OxOg4jiXZ7qBoM0uISGo= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= diff --git a/plugins/cmd/capabilities/log-event-trigger/main.go b/plugins/cmd/capabilities/log-event-trigger/main.go index 8abecf54aeb..7cf66f9c847 100644 --- a/plugins/cmd/capabilities/log-event-trigger/main.go +++ b/plugins/cmd/capabilities/log-event-trigger/main.go @@ -20,9 +20,9 @@ const ( ) type LogEventTriggerGRPCService struct { - trigger capabilities.TriggerCapability - s *loop.Server - config logevent.Config + triggerService *logevent.TriggerService + s *loop.Server + config logevent.Config } func main() { @@ -53,6 +53,10 @@ func (cs *LogEventTriggerGRPCService) Start(ctx context.Context) error { } func (cs *LogEventTriggerGRPCService) Close() error { + err := cs.triggerService.Close() + if err != nil { + return fmt.Errorf("error closing trigger service for chainID %s: %w", cs.config.ChainID, err) + } return nil } @@ -69,7 +73,7 @@ func (cs *LogEventTriggerGRPCService) Name() string { } func (cs *LogEventTriggerGRPCService) Infos(ctx context.Context) ([]capabilities.CapabilityInfo, error) { - triggerInfo, err := cs.trigger.Info(ctx) + triggerInfo, err := cs.triggerService.Info(ctx) if err != nil { return nil, err } @@ -94,23 +98,28 @@ func (cs *LogEventTriggerGRPCService) Initialise( var logEventConfig logevent.Config err := json.Unmarshal([]byte(config), &logEventConfig) if err != nil { - return fmt.Errorf("error decoding log_event_trigger config: %v", err) + return fmt.Errorf("error decoding log_event_trigger config: %w", err) } relayID := types.NewRelayID(logEventConfig.Network, logEventConfig.ChainID) relayer, err := relayerSet.Get(ctx, relayID) if err != nil { - return fmt.Errorf("error fetching relayer for chainID %s from relayerSet: %v", logEventConfig.ChainID, err) + return fmt.Errorf("error fetching relayer for chainID %s from relayerSet: %w", logEventConfig.ChainID, err) } // Set relayer and trigger in LogEventTriggerGRPCService cs.config = logEventConfig - cs.trigger, err = logevent.NewTriggerService(ctx, cs.s.Logger, relayer, logEventConfig) + triggerService, err := logevent.NewTriggerService(ctx, cs.s.Logger, relayer, logEventConfig) + if err != nil { + return fmt.Errorf("error creating trigger service for chainID %s: %w", logEventConfig.ChainID, err) + } + err = triggerService.Start(ctx) if err != nil { - return fmt.Errorf("error creating new trigger for chainID %s: %v", logEventConfig.ChainID, err) + return fmt.Errorf("error starting trigger service for chainID %s: %w", logEventConfig.ChainID, err) } + cs.triggerService = triggerService - if err := capabilityRegistry.Add(ctx, cs.trigger); err != nil { + if err := capabilityRegistry.Add(ctx, cs.triggerService); err != nil { return fmt.Errorf("error when adding cron trigger to the registry: %w", err) }