diff --git a/.changeset/real-tools-tap.md b/.changeset/real-tools-tap.md new file mode 100644 index 00000000000..37a3cf5e581 --- /dev/null +++ b/.changeset/real-tools-tap.md @@ -0,0 +1,5 @@ +--- +"chainlink": minor +--- + +#internal added tests for Chainwriter diff --git a/core/capabilities/targets/mocks/chain_reader.go b/core/capabilities/targets/mocks/chain_reader.go new file mode 100644 index 00000000000..306a305b8e5 --- /dev/null +++ b/core/capabilities/targets/mocks/chain_reader.go @@ -0,0 +1,189 @@ +// Code generated by mockery v2.43.0. DO NOT EDIT. + +package mocks + +import ( + context "context" + + query "github.com/smartcontractkit/chainlink-common/pkg/types/query" + mock "github.com/stretchr/testify/mock" + + types "github.com/smartcontractkit/chainlink-common/pkg/types" +) + +// ChainReader is an autogenerated mock type for the ChainReader type +type ChainReader struct { + mock.Mock +} + +// Bind provides a mock function with given fields: ctx, bindings +func (_m *ChainReader) Bind(ctx context.Context, bindings []types.BoundContract) error { + ret := _m.Called(ctx, bindings) + + if len(ret) == 0 { + panic("no return value specified for Bind") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, []types.BoundContract) error); ok { + r0 = rf(ctx, bindings) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Close provides a mock function with given fields: +func (_m *ChainReader) Close() error { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Close") + } + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// GetLatestValue provides a mock function with given fields: ctx, contractName, method, params, returnVal +func (_m *ChainReader) GetLatestValue(ctx context.Context, contractName string, method string, params interface{}, returnVal interface{}) error { + ret := _m.Called(ctx, contractName, method, params, returnVal) + + if len(ret) == 0 { + panic("no return value specified for GetLatestValue") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, string, interface{}, interface{}) error); ok { + r0 = rf(ctx, contractName, method, params, returnVal) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// HealthReport provides a mock function with given fields: +func (_m *ChainReader) HealthReport() map[string]error { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for HealthReport") + } + + var r0 map[string]error + if rf, ok := ret.Get(0).(func() map[string]error); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(map[string]error) + } + } + + return r0 +} + +// Name provides a mock function with given fields: +func (_m *ChainReader) Name() string { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Name") + } + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// QueryKey provides a mock function with given fields: ctx, contractName, filter, limitAndSort, sequenceDataType +func (_m *ChainReader) QueryKey(ctx context.Context, contractName string, filter query.KeyFilter, limitAndSort query.LimitAndSort, sequenceDataType interface{}) ([]types.Sequence, error) { + ret := _m.Called(ctx, contractName, filter, limitAndSort, sequenceDataType) + + if len(ret) == 0 { + panic("no return value specified for QueryKey") + } + + var r0 []types.Sequence + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, query.KeyFilter, query.LimitAndSort, interface{}) ([]types.Sequence, error)); ok { + return rf(ctx, contractName, filter, limitAndSort, sequenceDataType) + } + if rf, ok := ret.Get(0).(func(context.Context, string, query.KeyFilter, query.LimitAndSort, interface{}) []types.Sequence); ok { + r0 = rf(ctx, contractName, filter, limitAndSort, sequenceDataType) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]types.Sequence) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, query.KeyFilter, query.LimitAndSort, interface{}) error); ok { + r1 = rf(ctx, contractName, filter, limitAndSort, sequenceDataType) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Ready provides a mock function with given fields: +func (_m *ChainReader) Ready() error { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Ready") + } + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Start provides a mock function with given fields: _a0 +func (_m *ChainReader) Start(_a0 context.Context) error { + ret := _m.Called(_a0) + + if len(ret) == 0 { + panic("no return value specified for Start") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context) error); ok { + r0 = rf(_a0) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// NewChainReader creates a new instance of ChainReader. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewChainReader(t interface { + mock.TestingT + Cleanup(func()) +}) *ChainReader { + mock := &ChainReader{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/core/capabilities/targets/mocks/chain_writer.go b/core/capabilities/targets/mocks/chain_writer.go new file mode 100644 index 00000000000..379fbb87752 --- /dev/null +++ b/core/capabilities/targets/mocks/chain_writer.go @@ -0,0 +1,109 @@ +// Code generated by mockery v2.43.0. DO NOT EDIT. + +package mocks + +import ( + context "context" + big "math/big" + + mock "github.com/stretchr/testify/mock" + + types "github.com/smartcontractkit/chainlink-common/pkg/types" + + uuid "github.com/google/uuid" +) + +// ChainWriter is an autogenerated mock type for the ChainWriter type +type ChainWriter struct { + mock.Mock +} + +// GetFeeComponents provides a mock function with given fields: ctx +func (_m *ChainWriter) GetFeeComponents(ctx context.Context) (*types.ChainFeeComponents, error) { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for GetFeeComponents") + } + + var r0 *types.ChainFeeComponents + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) (*types.ChainFeeComponents, error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) *types.ChainFeeComponents); ok { + r0 = rf(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*types.ChainFeeComponents) + } + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetTransactionStatus provides a mock function with given fields: ctx, transactionID +func (_m *ChainWriter) GetTransactionStatus(ctx context.Context, transactionID uuid.UUID) (types.TransactionStatus, error) { + ret := _m.Called(ctx, transactionID) + + if len(ret) == 0 { + panic("no return value specified for GetTransactionStatus") + } + + var r0 types.TransactionStatus + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, uuid.UUID) (types.TransactionStatus, error)); ok { + return rf(ctx, transactionID) + } + if rf, ok := ret.Get(0).(func(context.Context, uuid.UUID) types.TransactionStatus); ok { + r0 = rf(ctx, transactionID) + } else { + r0 = ret.Get(0).(types.TransactionStatus) + } + + if rf, ok := ret.Get(1).(func(context.Context, uuid.UUID) error); ok { + r1 = rf(ctx, transactionID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// SubmitTransaction provides a mock function with given fields: ctx, contractName, method, args, transactionID, toAddress, meta, value +func (_m *ChainWriter) SubmitTransaction(ctx context.Context, contractName string, method string, args []interface{}, transactionID uuid.UUID, toAddress string, meta *types.TxMeta, value big.Int) error { + ret := _m.Called(ctx, contractName, method, args, transactionID, toAddress, meta, value) + + if len(ret) == 0 { + panic("no return value specified for SubmitTransaction") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, string, []interface{}, uuid.UUID, string, *types.TxMeta, big.Int) error); ok { + r0 = rf(ctx, contractName, method, args, transactionID, toAddress, meta, value) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// NewChainWriter creates a new instance of ChainWriter. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewChainWriter(t interface { + mock.TestingT + Cleanup(func()) +}) *ChainWriter { + mock := &ChainWriter{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/core/capabilities/targets/write_target_test.go b/core/capabilities/targets/write_target_test.go index acd61f7cdf1..d5b6c8feeeb 100644 --- a/core/capabilities/targets/write_target_test.go +++ b/core/capabilities/targets/write_target_test.go @@ -1,145 +1,137 @@ package targets_test import ( - "math/big" + "context" + "errors" + "github.com/ethereum/go-ethereum/common" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" "testing" "github.com/smartcontractkit/chainlink-common/pkg/capabilities" - relayermocks "github.com/smartcontractkit/chainlink-common/pkg/types/core/mocks" "github.com/smartcontractkit/chainlink-common/pkg/values" "github.com/smartcontractkit/chainlink/v2/core/capabilities/targets" - "github.com/smartcontractkit/chainlink/v2/core/chains/evm/txmgr" - txmmocks "github.com/smartcontractkit/chainlink/v2/core/chains/evm/txmgr/mocks" - "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" - evmmocks "github.com/smartcontractkit/chainlink/v2/core/chains/legacyevm/mocks" + "github.com/smartcontractkit/chainlink/v2/core/capabilities/targets/mocks" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/keystone/generated/forwarder" "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" - "github.com/smartcontractkit/chainlink/v2/core/internal/testutils/configtest" - "github.com/smartcontractkit/chainlink/v2/core/internal/testutils/evmtest" "github.com/smartcontractkit/chainlink/v2/core/logger" - "github.com/smartcontractkit/chainlink/v2/core/services/chainlink" - relayevm "github.com/smartcontractkit/chainlink/v2/core/services/relay/evm" - "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" ) var forwardABI = types.MustGetABI(forwarder.KeystoneForwarderMetaData.ABI) -func TestEvmWrite(t *testing.T) { - chain := evmmocks.NewChain(t) - - txManager := txmmocks.NewMockEvmTxManager(t) - chain.On("ID").Return(big.NewInt(11155111)) - chain.On("TxManager").Return(txManager) +//go:generate mockery --quiet --name ChainWriter --srcpkg=github.com/smartcontractkit/chainlink-common/pkg/types --output ./mocks/ --case=underscore +//go:generate mockery --quiet --name ChainReader --srcpkg=github.com/smartcontractkit/chainlink-common/pkg/types --output ./mocks/ --case=underscore +func TestWriteTarget(t *testing.T) { lggr := logger.TestLogger(t) - relayevm.NewRelayer(lggr, chain, evmrelayer.RelayerOpts{ - DS: db, - CSAETHKeystore: keyStore, - CapabilitiesRegistry: capabilities.NewRegistry(lggr), - }) - relayer := relayermocks.NewRelayer(lggr) + ctx := context.Background() - cfg := configtest.NewGeneralConfig(t, func(c *chainlink.Config, s *chainlink.Secrets) { - a := testutils.NewAddress() - addr, err := types.NewEIP55Address(a.Hex()) - require.NoError(t, err) - c.EVM[0].ChainWriter.FromAddress = &addr + cw := mocks.NewChainWriter(t) + cr := mocks.NewChainReader(t) - forwarderA := testutils.NewAddress() - forwarderAddr, err := types.NewEIP55Address(forwarderA.Hex()) - require.NoError(t, err) - c.EVM[0].ChainWriter.ForwarderAddress = &forwarderAddr - }) - evmcfg := evmtest.NewChainScopedConfig(t, cfg) - chain.On("Config").Return(evmcfg) + forwarderA := testutils.NewAddress() + forwarderAddr := forwarderA.Hex() - ctx := testutils.Context(t) - capability, err := targets.NewWriteTarget(ctx, relayer, chain, lggr) - require.NoError(t, err) + writeTarget := targets.NewWriteTarget(lggr, "Test", cr, cw, forwarderAddr) + require.NotNil(t, writeTarget) - config, err := values.NewMap(map[string]any{}) + config, err := values.NewMap(map[string]any{ + "Address": forwarderAddr, + }) require.NoError(t, err) - inputs, err := values.NewMap(map[string]any{ + validInputs, err := values.NewMap(map[string]any{ "report": []byte{1, 2, 3}, "signatures": [][]byte{}, }) require.NoError(t, err) - req := capabilities.CapabilityRequest{ - Metadata: capabilities.RequestMetadata{ - WorkflowID: "hello", - }, - Config: config, - Inputs: inputs, - } - - txManager.On("CreateTransaction", mock.Anything, mock.Anything).Return(txmgr.Tx{}, nil).Run(func(args mock.Arguments) { - req := args.Get(1).(txmgr.TxRequest) - payload := make(map[string]any) - method := forwardABI.Methods["report"] - err = method.Inputs.UnpackIntoMap(payload, req.EncodedPayload[4:]) - require.NoError(t, err) - require.Equal(t, []byte{0x1, 0x2, 0x3}, payload["rawReport"]) - require.Equal(t, [][]byte{}, payload["signatures"]) - }) + cr.On("GetLatestValue", mock.Anything, "forwarder", "getTransmitter", mock.Anything, mock.Anything).Return(nil).Run(func(args mock.Arguments) { + transmitter := args.Get(4).(*common.Address) + *transmitter = common.HexToAddress("0x0") + }).Twice() - ch, err := capability.Execute(ctx, req) - require.NoError(t, err) + cw.On("SubmitTransaction", mock.Anything, "forwarder", "report", mock.Anything, mock.Anything, forwarderAddr, mock.Anything, mock.Anything).Return(nil).Twice() - response := <-ch - require.Nil(t, response.Err) -} + t.Run("succeeds with valid report", func(t *testing.T) { + req := capabilities.CapabilityRequest{ + Metadata: capabilities.RequestMetadata{ + WorkflowID: "test-id", + }, + Config: config, + Inputs: validInputs, + } -func TestEvmWrite_EmptyReport(t *testing.T) { - chain := evmmocks.NewChain(t) + ch, err := writeTarget.Execute(ctx, req) + require.NoError(t, err) + response := <-ch + require.NotNil(t, response) + }) - txManager := txmmocks.NewMockEvmTxManager(t) - chain.On("ID").Return(big.NewInt(11155111)) - chain.On("TxManager").Return(txManager) + t.Run("succeeds with empty report", func(t *testing.T) { + emptyInputs, err := values.NewMap(map[string]any{ + "report": nil, + "signatures": [][]byte{}, + }) - cfg := configtest.NewGeneralConfig(t, func(c *chainlink.Config, s *chainlink.Secrets) { - a := testutils.NewAddress() - addr, err := types.NewEIP55Address(a.Hex()) require.NoError(t, err) - c.EVM[0].ChainWriter.FromAddress = &addr - - forwarderA := testutils.NewAddress() - forwarderAddr, err := types.NewEIP55Address(forwarderA.Hex()) + req := capabilities.CapabilityRequest{ + Metadata: capabilities.RequestMetadata{ + WorkflowExecutionID: "test-id", + }, + Config: config, + Inputs: emptyInputs, + } + + ch, err := writeTarget.Execute(ctx, req) require.NoError(t, err) - c.EVM[0].ChainWriter.ForwarderAddress = &forwarderAddr + response := <-ch + require.Nil(t, response.Value) }) - evmcfg := evmtest.NewChainScopedConfig(t, cfg) - chain.On("Config").Return(evmcfg) - ctx := testutils.Context(t) - capability, err := targets.NewWriteTarget(ctx, relayer, chain, logger.TestLogger(t)) - require.NoError(t, err) - - config, err := values.NewMap(map[string]any{ - "abi": "receive(report bytes)", - "params": []any{"$(report)"}, + t.Run("fails when ChainReader's GetLatestValue returns error", func(t *testing.T) { + req := capabilities.CapabilityRequest{ + Metadata: capabilities.RequestMetadata{ + WorkflowID: "test-id", + }, + Config: config, + Inputs: validInputs, + } + cr.On("GetLatestValue", mock.Anything, "forwarder", "getTransmitter", mock.Anything, mock.Anything).Return(errors.New("reader error")) + + _, err = writeTarget.Execute(ctx, req) + require.Error(t, err) }) - require.NoError(t, err) - inputs, err := values.NewMap(map[string]any{ - "report": nil, + t.Run("fails when ChainWriter's SubmitTransaction returns error", func(t *testing.T) { + req := capabilities.CapabilityRequest{ + Metadata: capabilities.RequestMetadata{ + WorkflowID: "test-id", + }, + Config: config, + Inputs: validInputs, + } + cw.On("SubmitTransaction", mock.Anything, "forwarder", "report", mock.Anything, mock.Anything, forwarderAddr, mock.Anything, mock.Anything).Return(errors.New("writer error")) + + _, err = writeTarget.Execute(ctx, req) + require.Error(t, err) }) - require.NoError(t, err) - - req := capabilities.CapabilityRequest{ - Metadata: capabilities.RequestMetadata{ - WorkflowID: "hello", - }, - Config: config, - Inputs: inputs, - } - ch, err := capability.Execute(ctx, req) - require.NoError(t, err) + t.Run("fails with invalid config", func(t *testing.T) { + invalidConfig, err := values.NewMap(map[string]any{ + "Address": "invalid-address", + }) + require.NoError(t, err) - response := <-ch - require.Nil(t, response.Err) + req := capabilities.CapabilityRequest{ + Metadata: capabilities.RequestMetadata{ + WorkflowID: "test-id", + }, + Config: invalidConfig, + Inputs: validInputs, + } + _, err = writeTarget.Execute(ctx, req) + require.Error(t, err) + }) } diff --git a/core/services/relay/evm/write_target_test.go b/core/services/relay/evm/write_target_test.go new file mode 100644 index 00000000000..73e9b5e8ad9 --- /dev/null +++ b/core/services/relay/evm/write_target_test.go @@ -0,0 +1,205 @@ +package evm_test + +import ( + "errors" + "fmt" + "github.com/smartcontractkit/chainlink-common/pkg/values" + "github.com/smartcontractkit/chainlink/v2/core/internal/cltest" + "github.com/smartcontractkit/chainlink/v2/core/internal/testutils/pgtest" + "github.com/smartcontractkit/chainlink/v2/core/services/relay/evm" + "math/big" + "testing" + + "github.com/smartcontractkit/chainlink-common/pkg/capabilities" + evmcapabilities "github.com/smartcontractkit/chainlink/v2/core/capabilities" + evmclimocks "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client/mocks" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/txmgr" + txmmocks "github.com/smartcontractkit/chainlink/v2/core/chains/evm/txmgr/mocks" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" + evmmocks "github.com/smartcontractkit/chainlink/v2/core/chains/legacyevm/mocks" + relayevm "github.com/smartcontractkit/chainlink/v2/core/services/relay/evm" + + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/keystone/generated/forwarder" + "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" + "github.com/smartcontractkit/chainlink/v2/core/internal/testutils/configtest" + "github.com/smartcontractkit/chainlink/v2/core/internal/testutils/evmtest" + "github.com/smartcontractkit/chainlink/v2/core/logger" + "github.com/smartcontractkit/chainlink/v2/core/services/chainlink" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +var forwardABI = types.MustGetABI(forwarder.KeystoneForwarderMetaData.ABI) + +func TestEvmWrite(t *testing.T) { + chain := evmmocks.NewChain(t) + txManager := txmmocks.NewMockEvmTxManager(t) + evmClient := evmclimocks.NewClient(t) + + // This probably isn't the best way to do this, but couldn't find a simpler way to mock the CallContract response + var mockCall []byte + for i := 0; i < 32; i++ { + mockCall = append(mockCall, byte(0)) + } + evmClient.On("CallContract", mock.Anything, mock.Anything, mock.Anything).Return(mockCall, nil) + + chain.On("ID").Return(big.NewInt(11155111)) + chain.On("TxManager").Return(txManager) + chain.On("LogPoller").Return(nil) + chain.On("Client").Return(evmClient) + + db := pgtest.NewSqlxDB(t) + keyStore := cltest.NewKeyStore(t, db) + + lggr := logger.TestLogger(t) + relayer, err := relayevm.NewRelayer(lggr, chain, relayevm.RelayerOpts{ + DS: db, + CSAETHKeystore: keyStore, + CapabilitiesRegistry: evmcapabilities.NewRegistry(lggr), + }) + require.NoError(t, err) + + cfg := configtest.NewGeneralConfig(t, func(c *chainlink.Config, s *chainlink.Secrets) { + a := testutils.NewAddress() + addr, err := types.NewEIP55Address(a.Hex()) + require.NoError(t, err) + c.EVM[0].ChainWriter.FromAddress = &addr + + forwarderA := testutils.NewAddress() + forwarderAddr, err := types.NewEIP55Address(forwarderA.Hex()) + require.NoError(t, err) + c.EVM[0].ChainWriter.ForwarderAddress = &forwarderAddr + }) + evmCfg := evmtest.NewChainScopedConfig(t, cfg) + fmt.Println(evmCfg) + chain.On("Config").Return(evmCfg) + + txManager.On("CreateTransaction", mock.Anything, mock.Anything).Return(txmgr.Tx{}, nil).Run(func(args mock.Arguments) { + req := args.Get(1).(txmgr.TxRequest) + payload := make(map[string]any) + method := forwardABI.Methods["report"] + err = method.Inputs.UnpackIntoMap(payload, req.EncodedPayload[4:]) + require.NoError(t, err) + require.Equal(t, []byte{0x1, 0x2, 0x3}, payload["rawReport"]) + require.Equal(t, [][]byte{}, payload["signatures"]) + }).Twice() + + t.Run("succeeds with valid report", func(t *testing.T) { + ctx := testutils.Context(t) + capability, err := evm.NewWriteTarget(ctx, relayer, chain, lggr) + require.NoError(t, err) + + config, err := values.NewMap(map[string]any{ + "Address": evmCfg.EVM().ChainWriter().ForwarderAddress().String(), + }) + require.NoError(t, err) + + inputs, err := values.NewMap(map[string]any{ + "report": []byte{1, 2, 3}, + "signatures": [][]byte{}, + }) + require.NoError(t, err) + + req := capabilities.CapabilityRequest{ + Metadata: capabilities.RequestMetadata{ + WorkflowID: "test-id", + }, + Config: config, + Inputs: inputs, + } + + ch, err := capability.Execute(ctx, req) + require.NoError(t, err) + + response := <-ch + require.Nil(t, response.Err) + }) + + t.Run("succeeds with empty report", func(t *testing.T) { + ctx := testutils.Context(t) + capability, err := evm.NewWriteTarget(ctx, relayer, chain, logger.TestLogger(t)) + require.NoError(t, err) + + config, err := values.NewMap(map[string]any{ + "abi": "receive(report bytes)", + "params": []any{"$(report)"}, + "Address": evmCfg.EVM().ChainWriter().ForwarderAddress().String(), + }) + require.NoError(t, err) + + inputs, err := values.NewMap(map[string]any{ + "report": nil, + }) + require.NoError(t, err) + + req := capabilities.CapabilityRequest{ + Metadata: capabilities.RequestMetadata{ + WorkflowID: "test-id", + }, + Config: config, + Inputs: inputs, + } + + ch, err := capability.Execute(ctx, req) + require.NoError(t, err) + + response := <-ch + require.Nil(t, response.Err) + }) + + t.Run("fails with invalid config", func(t *testing.T) { + ctx := testutils.Context(t) + capability, err := evm.NewWriteTarget(ctx, relayer, chain, logger.TestLogger(t)) + + invalidConfig, err := values.NewMap(map[string]any{ + "Address": "invalid-address", + }) + require.NoError(t, err) + + inputs, err := values.NewMap(map[string]any{ + "report": nil, + }) + require.NoError(t, err) + + req := capabilities.CapabilityRequest{ + Metadata: capabilities.RequestMetadata{ + WorkflowID: "test-id", + }, + Config: invalidConfig, + Inputs: inputs, + } + + _, err = capability.Execute(ctx, req) + require.Error(t, err) + }) + + t.Run("fails when TXM CreateTransaction returns error", func(t *testing.T) { + ctx := testutils.Context(t) + capability, err := evm.NewWriteTarget(ctx, relayer, chain, logger.TestLogger(t)) + require.NoError(t, err) + + config, err := values.NewMap(map[string]any{ + "Address": evmCfg.EVM().ChainWriter().ForwarderAddress().String(), + }) + require.NoError(t, err) + + inputs, err := values.NewMap(map[string]any{ + "report": []byte{1, 2, 3}, + "signatures": [][]byte{}, + }) + require.NoError(t, err) + + req := capabilities.CapabilityRequest{ + Metadata: capabilities.RequestMetadata{ + WorkflowID: "test-id", + }, + Config: config, + Inputs: inputs, + } + + txManager.On("CreateTransaction", mock.Anything, mock.Anything).Return(txmgr.Tx{}, errors.New("TXM error")) + + _, err = capability.Execute(ctx, req) + require.Error(t, err) + }) +}