diff --git a/pkg/chainaccessor/default_accessor.go b/pkg/chainaccessor/default_accessor.go index baef2a60c..9c4ea743a 100644 --- a/pkg/chainaccessor/default_accessor.go +++ b/pkg/chainaccessor/default_accessor.go @@ -8,6 +8,7 @@ import ( "strconv" "time" + "github.com/ethereum/go-ethereum/common/hexutil" "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink-common/pkg/types" cciptypes "github.com/smartcontractkit/chainlink-common/pkg/types/ccipocr3" @@ -378,6 +379,17 @@ func (l *DefaultAccessor) MsgsBetweenSeqNums( } msg.Message.Header.OnRamp = onRampAddress + + // Populate TxHash from Sequence item + if len(item.TxHash) > 0 { + msg.Message.Header.TxHash = hexutil.Encode(item.TxHash) + } else { + // TxHash is empty - log warning and leave it empty + lggr.Warnw("transaction hash is empty", + "cursor", item.Cursor, + "seqNum", msg.Message.Header.SequenceNumber) + } + msgs = append(msgs, msg.Message) } diff --git a/pkg/chainaccessor/default_accessor_test.go b/pkg/chainaccessor/default_accessor_test.go index 17c7afb88..f22232d63 100644 --- a/pkg/chainaccessor/default_accessor_test.go +++ b/pkg/chainaccessor/default_accessor_test.go @@ -3,6 +3,7 @@ package chainaccessor import ( "context" "fmt" + "math/big" "reflect" "testing" @@ -381,3 +382,206 @@ func TestDefaultAccessor_GetSourceChainsConfig(t *testing.T) { require.NoError(t, err) assert.Equal(t, []byte(expectedRouterB), cfgB.Router) } + +// Helper function to create a valid SendRequestedEvent for testing +func createValidSendRequestedEvent(seqNum cciptypes.SeqNum) *SendRequestedEvent { + return &SendRequestedEvent{ + DestChainSelector: chainB, + SequenceNumber: seqNum, + Message: cciptypes.Message{ + Header: cciptypes.RampMessageHeader{ + SourceChainSelector: chainA, + DestChainSelector: chainB, + SequenceNumber: seqNum, + MessageID: cciptypes.Bytes32{byte(seqNum)}, + }, + Sender: cciptypes.UnknownAddress("sender"), + Receiver: cciptypes.UnknownAddress("receiver"), + FeeToken: cciptypes.UnknownAddress("feeToken"), + FeeTokenAmount: cciptypes.NewBigInt(big.NewInt(100)), + }, + } +} + +func TestMsgsBetweenSeqNums(t *testing.T) { + tests := []struct { + name string + seqNumRange cciptypes.SeqNumRange + destChainSelector cciptypes.ChainSelector + sequences []types.Sequence + expectedError bool + expectedMsgCount int + validateTxHash func(t *testing.T, msgs []cciptypes.Message) + }{ + { + name: "TxHash populated from item.TxHash", + seqNumRange: cciptypes.NewSeqNumRange(1, 3), + destChainSelector: chainB, + sequences: []types.Sequence{ + { + Cursor: "100-1-0xabc123", + TxHash: []byte{0xab, 0xcd, 0xef, 0x12, 0x34, 0x56}, + Data: createValidSendRequestedEvent(1), + }, + { + Cursor: "100-2-0xdef456", + TxHash: []byte{0xde, 0xad, 0xbe, 0xef, 0x78, 0x90}, + Data: createValidSendRequestedEvent(2), + }, + }, + expectedError: false, + expectedMsgCount: 2, + validateTxHash: func(t *testing.T, msgs []cciptypes.Message) { + require.Len(t, msgs, 2) + // TxHash should be populated from item.TxHash with 0x prefix + assert.Equal(t, "0xabcdef123456", msgs[0].Header.TxHash) + assert.Equal(t, "0xdeadbeef7890", msgs[1].Header.TxHash) + }, + }, + { + name: "Mixed: some with item.TxHash, some without", + seqNumRange: cciptypes.NewSeqNumRange(1, 3), + destChainSelector: chainB, + sequences: []types.Sequence{ + { + Cursor: "100-1-0xcursor111", + TxHash: []byte{0x11, 0x22, 0x33}, // Has TxHash + Data: createValidSendRequestedEvent(1), + }, + { + Cursor: "100-2-0xcursor222", + TxHash: nil, // No TxHash + Data: createValidSendRequestedEvent(2), + }, + { + Cursor: "100-3-0xcursor333", + TxHash: []byte{0x44, 0x55, 0x66}, // Has TxHash + Data: createValidSendRequestedEvent(3), + }, + }, + expectedError: false, + expectedMsgCount: 3, + validateTxHash: func(t *testing.T, msgs []cciptypes.Message) { + require.Len(t, msgs, 3) + assert.Equal(t, "0x112233", msgs[0].Header.TxHash) // From item.TxHash + assert.Equal(t, "", msgs[1].Header.TxHash) // Empty - no TxHash provided + assert.Equal(t, "0x445566", msgs[2].Header.TxHash) // From item.TxHash + }, + }, + { + name: "Empty TxHash when item.TxHash is not provided", + seqNumRange: cciptypes.NewSeqNumRange(1, 1), + destChainSelector: chainB, + sequences: []types.Sequence{ + { + Cursor: "100-1-0xabc123", + TxHash: nil, // No TxHash + Data: createValidSendRequestedEvent(1), + }, + }, + expectedError: false, + expectedMsgCount: 1, + validateTxHash: func(t *testing.T, msgs []cciptypes.Message) { + require.Len(t, msgs, 1) + // TxHash should be empty when item.TxHash is not provided + assert.Equal(t, "", msgs[0].Header.TxHash) + }, + }, + { + name: "Filter out invalid sequence number", + seqNumRange: cciptypes.NewSeqNumRange(1, 2), + destChainSelector: chainB, + sequences: []types.Sequence{ + { + Cursor: "100-1-0xabc123", + TxHash: []byte{0xab, 0xcd}, + Data: createValidSendRequestedEvent(1), + }, + { + Cursor: "100-2-0xdef456", + TxHash: []byte{0xde, 0xef}, + Data: createValidSendRequestedEvent(10), // Out of range + }, + }, + expectedError: false, + expectedMsgCount: 1, + validateTxHash: func(t *testing.T, msgs []cciptypes.Message) { + require.Len(t, msgs, 1) + assert.Equal(t, "0xabcd", msgs[0].Header.TxHash) + }, + }, + { + name: "Wrong data type in sequence", + seqNumRange: cciptypes.NewSeqNumRange(1, 2), + destChainSelector: chainB, + sequences: []types.Sequence{ + { + Cursor: "100-1-0xabc123", + TxHash: []byte{0xab, 0xcd}, + Data: "invalid data type", // Not SendRequestedEvent + }, + }, + expectedError: true, + expectedMsgCount: 0, + validateTxHash: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Setup mocks + mockReader := reader_mocks.NewMockExtended(t) + mockWriter := writer_mocks.NewMockContractWriter(t) + codec := internal.NewMockAddressCodecHex(t) + + accessor := &DefaultAccessor{ + lggr: logger.Test(t), + chainSelector: chainA, + contractReader: mockReader, + contractWriter: mockWriter, + addrCodec: codec, + } + + // Setup mock expectations + mockReader.On("ExtendedQueryKey", mock.Anything, mock.Anything, + mock.Anything, mock.Anything, mock.Anything). + Return(tt.sequences, nil).Once() + + // Setup GetBindings mock for GetContractAddress call + // The address string will be converted to bytes by the codec + onRampAddressStr := "0x1234567890abcdef" + onRampAddress := cciptypes.UnknownAddress{0x12, 0x34, 0x56, 0x78, 0x90, 0xab, 0xcd, 0xef} + bindings := []contractreader.ExtendedBoundContract{ + { + Binding: types.BoundContract{ + Name: consts.ContractNameOnRamp, + Address: onRampAddressStr, + }, + }, + } + mockReader.On("GetBindings", consts.ContractNameOnRamp). + Return(bindings).Once() + + // Execute test + msgs, err := accessor.MsgsBetweenSeqNums(context.Background(), tt.destChainSelector, tt.seqNumRange) + + // Verify results + if tt.expectedError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Len(t, msgs, tt.expectedMsgCount) + if tt.validateTxHash != nil { + tt.validateTxHash(t, msgs) + } + // Verify OnRamp is always set + for _, msg := range msgs { + assert.Equal(t, onRampAddress, msg.Header.OnRamp) + } + } + + // Verify all mock expectations were met + mockReader.AssertExpectations(t) + }) + } +}