diff --git a/.changeset/serious-cups-yawn.md b/.changeset/serious-cups-yawn.md new file mode 100644 index 00000000000..3ba442ec639 --- /dev/null +++ b/.changeset/serious-cups-yawn.md @@ -0,0 +1,5 @@ +--- +"chainlink": minor +--- + +add new commit and exec codec using evm codec #added diff --git a/core/capabilities/ccip/ccipevm/commitcodecv2.go b/core/capabilities/ccip/ccipevm/commitcodecv2.go new file mode 100644 index 00000000000..16e98765910 --- /dev/null +++ b/core/capabilities/ccip/ccipevm/commitcodecv2.go @@ -0,0 +1,107 @@ +package ccipevm + +import ( + "context" + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/common" + cciptypes "github.com/smartcontractkit/chainlink-ccip/pkg/types/ccipocr3" + commoncodec "github.com/smartcontractkit/chainlink-common/pkg/codec" + commontypes "github.com/smartcontractkit/chainlink-common/pkg/types" + "github.com/smartcontractkit/chainlink/v2/core/services/relay/evm/codec" + "github.com/smartcontractkit/chainlink/v2/core/services/relay/evm/types" +) + +const commitReportABI = `[{"components":[{"name":"chainSel","type":"uint64","internalType":"uint64"},{"name":"onRampAddress","type":"bytes","internalType":"bytes"},{"name":"seqNumsRange","type":"uint64[2]","internalType":"uint64[2]"},{"name":"merkleRoot","type":"bytes32","internalType":"bytes32"}],"name":"merkleRoots","type":"tuple[]","internalType":"struct MerkleRootChain[]"},{"components":[{"components":[{"name":"tokenID","type":"string","internalType":"string"},{"name":"price","type":"uint256","internalType":"uint256"}],"name":"tokenPriceUpdates","type":"tuple[]","internalType":"struct TokenPrice[]"},{"components":[{"name":"chainSel","type":"uint64","internalType":"uint64"},{"name":"gasPrice","type":"uint256","internalType":"uint256"}],"name":"gasPriceUpdates","type":"tuple[]","internalType":"struct GasPriceChain[]"}],"name":"priceUpdates","type":"tuple","internalType":"struct PriceUpdates"},{"components":[{"name":"r","type":"bytes32","internalType":"bytes32"},{"name":"s","type":"bytes32","internalType":"bytes32"}],"name":"rmnSignatures","type":"tuple[]"}]` + +var commitCodecConfig = types.CodecConfig{ + Configs: map[string]types.ChainCodecConfig{ + "CommitPluginReport": { + TypeABI: commitReportABI, + ModifierConfigs: commoncodec.ModifiersConfig{ + &commoncodec.WrapperModifierConfig{Fields: map[string]string{ + "PriceUpdates.TokenPriceUpdates.Price": "Int", + "PriceUpdates.GasPriceUpdates.GasPrice": "Int", + }}, + }, + }, + }, +} + +// CommitPluginCodecV2 is a codec for encoding and decoding commit plugin reports using generic evm codec +type CommitPluginCodecV2 struct { + codec commontypes.RemoteCodec +} + +func NewCommitPluginCodecV2() (*CommitPluginCodecV2, error) { + cd, err := codec.NewCodec(commitCodecConfig) + if err != nil { + return nil, err + } + return &CommitPluginCodecV2{codec: cd}, nil +} + +func validateReport(report cciptypes.CommitPluginReport) error { + for _, update := range report.PriceUpdates.TokenPriceUpdates { + if !common.IsHexAddress(string(update.TokenID)) { + return fmt.Errorf("invalid token address: %s", update.TokenID) + } + if update.Price.IsEmpty() { + return fmt.Errorf("empty price for token: %s", update.TokenID) + } + } + + for _, update := range report.PriceUpdates.GasPriceUpdates { + if update.GasPrice.IsEmpty() { + return fmt.Errorf("empty gas price for chain: %d", update.ChainSel) + } + } + + return nil +} + +func (c *CommitPluginCodecV2) Encode(ctx context.Context, report cciptypes.CommitPluginReport) ([]byte, error) { + if err := validateReport(report); err != nil { + return nil, err + } + + return c.codec.Encode(ctx, report, "CommitPluginReport") +} + +func commitPostProcess(report *cciptypes.CommitPluginReport) error { + for index, update := range report.PriceUpdates.TokenPriceUpdates { + if !common.IsHexAddress(string(update.TokenID)) { + return fmt.Errorf("invalid token address: %s", update.TokenID) + } + if update.Price.IsEmpty() || update.Price.Int64() == 0 { + report.PriceUpdates.TokenPriceUpdates[index].Price = cciptypes.NewBigInt(big.NewInt(0)) + } + } + + for idx, update := range report.PriceUpdates.GasPriceUpdates { + if update.GasPrice.IsEmpty() || update.GasPrice.Int64() == 0 { + report.PriceUpdates.GasPriceUpdates[idx].GasPrice = cciptypes.NewBigInt(big.NewInt(0)) + } + } + + return nil +} + +func (c *CommitPluginCodecV2) Decode(ctx context.Context, bytes []byte) (cciptypes.CommitPluginReport, error) { + report := cciptypes.CommitPluginReport{} + + err := c.codec.Decode(ctx, bytes, &report, "CommitPluginReport") + if err != nil { + return report, err + } + + if err = commitPostProcess(&report); err != nil { + return report, err + } + + return report, nil +} + +// Ensure CommitPluginCodec implements the CommitPluginCodec interface +var _ cciptypes.CommitPluginCodec = (*CommitPluginCodecV2)(nil) diff --git a/core/capabilities/ccip/ccipevm/commitcodecv2_test.go b/core/capabilities/ccip/ccipevm/commitcodecv2_test.go new file mode 100644 index 00000000000..3a4c3da528c --- /dev/null +++ b/core/capabilities/ccip/ccipevm/commitcodecv2_test.go @@ -0,0 +1,99 @@ +package ccipevm + +import ( + "math/big" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + cciptypes "github.com/smartcontractkit/chainlink-ccip/pkg/types/ccipocr3" + "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" +) + +func TestCommitPluginCodecV2(t *testing.T) { + testCases := []struct { + name string + report func(report cciptypes.CommitPluginReport) cciptypes.CommitPluginReport + expErr bool + }{ + { + name: "base report", + report: func(report cciptypes.CommitPluginReport) cciptypes.CommitPluginReport { + return report + }, + }, + { + name: "empty token address", + report: func(report cciptypes.CommitPluginReport) cciptypes.CommitPluginReport { + report.PriceUpdates.TokenPriceUpdates[0].TokenID = "" + return report + }, + expErr: true, + }, + { + name: "empty merkle root", + report: func(report cciptypes.CommitPluginReport) cciptypes.CommitPluginReport { + report.MerkleRoots[0].MerkleRoot = cciptypes.Bytes32{} + return report + }, + }, + { + name: "zero token price", + report: func(report cciptypes.CommitPluginReport) cciptypes.CommitPluginReport { + report.PriceUpdates.TokenPriceUpdates[0].Price = cciptypes.NewBigInt(big.NewInt(0)) + return report + }, + }, + { + name: "zero gas price", + report: func(report cciptypes.CommitPluginReport) cciptypes.CommitPluginReport { + report.PriceUpdates.GasPriceUpdates[0].GasPrice = cciptypes.NewBigInt(big.NewInt(0)) + return report + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + report := tc.report(randomCommitReport()) + commitCodec, err := NewCommitPluginCodecV2() + require.NoError(t, err) + ctx := testutils.Context(t) + encodedReport, err := commitCodec.Encode(ctx, report) + if tc.expErr { + assert.Error(t, err) + return + } + require.NoError(t, err) + decodedReport, err := commitCodec.Decode(ctx, encodedReport) + require.NoError(t, err) + require.Equal(t, report, decodedReport) + }) + } +} + +func BenchmarkCommitPluginCodecV2_Encode(b *testing.B) { + commitCodec, err := NewCommitPluginCodecV2() + require.NoError(b, err) + ctx := testutils.Context(b) + + rep := randomCommitReport() + for i := 0; i < b.N; i++ { + _, err := commitCodec.Encode(ctx, rep) + require.NoError(b, err) + } +} + +func BenchmarkCommitPluginCodecV2_Decode(b *testing.B) { + commitCodec, err := NewCommitPluginCodecV2() + require.NoError(b, err) + ctx := testutils.Context(b) + encodedReport, err := commitCodec.Encode(ctx, randomCommitReport()) + require.NoError(b, err) + + for i := 0; i < b.N; i++ { + _, err := commitCodec.Decode(ctx, encodedReport) + require.NoError(b, err) + } +} diff --git a/core/capabilities/ccip/ccipevm/executecodec_test.go b/core/capabilities/ccip/ccipevm/executecodec_test.go index 53b215d8ed9..de926a028b3 100644 --- a/core/capabilities/ccip/ccipevm/executecodec_test.go +++ b/core/capabilities/ccip/ccipevm/executecodec_test.go @@ -8,7 +8,6 @@ import ( "github.com/ethereum/go-ethereum/accounts/abi/bind/backends" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core" - cciptypes "github.com/smartcontractkit/chainlink-ccip/pkg/types/ccipocr3" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/assets" @@ -68,6 +67,7 @@ var randomExecuteReport = func(t *testing.T, d *testSetupData) cciptypes.Execute ExtraArgs: extraArgs, FeeToken: utils.RandomAddress().Bytes(), FeeTokenAmount: cciptypes.NewBigInt(utils.RandUint256()), + FeeValueJuels: cciptypes.NewBigInt(utils.RandUint256()), TokenAmounts: tokenAmounts, } } @@ -156,6 +156,7 @@ func TestExecutePluginCodecV1(t *testing.T) { report.ChainReports[i].Messages[j].FeeToken = cciptypes.UnknownAddress{} report.ChainReports[i].Messages[j].ExtraArgs = cciptypes.Bytes{} report.ChainReports[i].Messages[j].FeeTokenAmount = cciptypes.BigInt{} + report.ChainReports[i].Messages[j].FeeValueJuels = cciptypes.BigInt{} } } diff --git a/core/capabilities/ccip/ccipevm/executecodecv2.go b/core/capabilities/ccip/ccipevm/executecodecv2.go new file mode 100644 index 00000000000..0742be5b451 --- /dev/null +++ b/core/capabilities/ccip/ccipevm/executecodecv2.go @@ -0,0 +1,137 @@ +package ccipevm + +import ( + "context" + "errors" + "fmt" + "math/big" + + commoncodec "github.com/smartcontractkit/chainlink-common/pkg/codec" + commontypes "github.com/smartcontractkit/chainlink-common/pkg/types" + "github.com/smartcontractkit/chainlink/v2/core/services/relay/evm/codec" + "github.com/smartcontractkit/chainlink/v2/core/services/relay/evm/types" + + cciptypes "github.com/smartcontractkit/chainlink-ccip/pkg/types/ccipocr3" +) + +const execReportABI = `[{"name":"chainReports","type":"tuple[]","components":[{"name":"sourceChainSelector","type":"uint64","internalType":"uint64"},{"name":"messages","type":"tuple[]","components":[{"name":"header","type":"tuple","components":[{"name":"messageID","type":"bytes32","internalType":"bytes32"},{"name":"sourceChainSelector","type":"uint64","internalType":"uint64"},{"name":"destChainSelector","type":"uint64","internalType":"uint64"},{"name":"sequenceNumber","type":"uint64","internalType":"uint64"},{"name":"nonce","type":"uint64","internalType":"uint64"},{"name":"msgHash","type":"bytes32","internalType":"bytes32"},{"name":"onRamp","type":"bytes","internalType":"bytes"}]},{"name":"data","type":"bytes","internalType":"bytes"},{"name":"sender","type":"bytes","internalType":"bytes"},{"name":"receiver","type":"bytes","internalType":"bytes"},{"name":"extraArgs","type":"bytes","internalType":"bytes"},{"name":"feeToken","type":"bytes","internalType":"bytes"},{"name":"feeTokenAmount","type":"uint256","internalType":"uint256"},{"name":"feeValueJuels","type":"uint256","internalType":"uint256"},{"name":"tokenAmounts","type":"tuple[]","components":[{"name":"sourcePoolAddress","type":"bytes","internalType":"bytes"},{"name":"destTokenAddress","type":"bytes","internalType":"bytes"},{"name":"extraData","type":"bytes","internalType":"bytes"},{"name":"amount","type":"uint256","internalType":"uint256"},{"name":"destExecData","type":"bytes","internalType":"bytes"}]}]},{"name":"offchainTokenData","type":"bytes[][]","internalType":"bytes[][]"},{"name":"proofs","type":"bytes32[]","internalType":"bytes32[]"},{"name":"proofFlagBits","type":"uint256","internalType":"uint256"}]}]` + +var execCodecConfig = types.CodecConfig{ + Configs: map[string]types.ChainCodecConfig{ + "ExecPluginReport": { + TypeABI: execReportABI, + ModifierConfigs: commoncodec.ModifiersConfig{ + &commoncodec.WrapperModifierConfig{ + Fields: map[string]string{ + "ChainReports.Messages.FeeTokenAmount": "Int", + "ChainReports.Messages.FeeValueJuels": "Int", + "ChainReports.Messages.TokenAmounts.Amount": "Int", + "ChainReports.ProofFlagBits": "Int", + }, + }, + }, + }, + }, +} + +// ExecutePluginCodecV2 is a codec for encoding and decoding execute plugin reports with generic codec +type ExecutePluginCodecV2 struct { + codec commontypes.RemoteCodec +} + +func NewExecutePluginCodecV2() (*ExecutePluginCodecV2, error) { + cd, err := codec.NewCodec(execCodecConfig) + if err != nil { + return nil, err + } + return &ExecutePluginCodecV2{codec: cd}, nil +} + +func validate(report cciptypes.ExecutePluginReport) error { + for i, chainReport := range report.ChainReports { + if chainReport.ProofFlagBits.IsEmpty() { + return errors.New("proof flag bits are empty") + } + + for j, message := range chainReport.Messages { + // optional fields + if message.FeeToken == nil { + report.ChainReports[i].Messages[j].FeeToken = []byte{} + } + + if message.FeeValueJuels.IsEmpty() { + report.ChainReports[i].Messages[j].FeeValueJuels = cciptypes.NewBigInt(big.NewInt(0)) + } + + if message.FeeTokenAmount.IsEmpty() { + report.ChainReports[i].Messages[j].FeeTokenAmount = cciptypes.NewBigInt(big.NewInt(0)) + } + + // required fields + if message.Sender == nil { + return errors.New("message sender is nil") + } + + for _, tokenAmount := range message.TokenAmounts { + if tokenAmount.Amount.IsEmpty() { + return fmt.Errorf("empty amount for token: %s", tokenAmount.DestTokenAddress) + } + + _, err := abiDecodeUint32(tokenAmount.DestExecData) + if err != nil { + return fmt.Errorf("decode dest gas amount: %w", err) + } + } + + _, err := decodeExtraArgsV1V2(message.ExtraArgs) + if err != nil { + return fmt.Errorf("decode extra args to get gas limit: %w", err) + } + } + } + + return nil +} + +func (e *ExecutePluginCodecV2) Encode(ctx context.Context, report cciptypes.ExecutePluginReport) ([]byte, error) { + if err := validate(report); err != nil { + return nil, err + } + + return e.codec.Encode(ctx, report, "ExecPluginReport") +} + +func execPostProcess(report *cciptypes.ExecutePluginReport) error { + if len(report.ChainReports) == 0 { + return errors.New("chain reports is empty") + } + + for i, evmChainReport := range report.ChainReports { + for j := range evmChainReport.Messages { + report.ChainReports[i].Messages[j].Header.MsgHash = cciptypes.Bytes32{} + report.ChainReports[i].Messages[j].Header.OnRamp = cciptypes.UnknownAddress{} + report.ChainReports[i].Messages[j].ExtraArgs = cciptypes.Bytes{} + report.ChainReports[i].Messages[j].FeeToken = cciptypes.UnknownAddress{} + report.ChainReports[i].Messages[j].FeeTokenAmount = cciptypes.BigInt{} + } + } + + return nil +} + +func (e *ExecutePluginCodecV2) Decode(ctx context.Context, encodedReport []byte) (cciptypes.ExecutePluginReport, error) { + report := cciptypes.ExecutePluginReport{} + err := e.codec.Decode(ctx, encodedReport, &report, "ExecPluginReport") + if err != nil { + return report, err + } + + if err = execPostProcess(&report); err != nil { + return report, err + } + + return report, err +} + +// Ensure ExecutePluginCodec implements the ExecutePluginCodec interface +var _ cciptypes.ExecutePluginCodec = (*ExecutePluginCodecV2)(nil) diff --git a/core/capabilities/ccip/ccipevm/executecodecv2_test.go b/core/capabilities/ccip/ccipevm/executecodecv2_test.go new file mode 100644 index 00000000000..7722f01f0f3 --- /dev/null +++ b/core/capabilities/ccip/ccipevm/executecodecv2_test.go @@ -0,0 +1,96 @@ +package ccipevm + +import ( + "testing" + + cciptypes "github.com/smartcontractkit/chainlink-ccip/pkg/types/ccipocr3" + "github.com/smartcontractkit/chainlink/v2/core/services/relay/evm/codec" + + "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestExecuteCodec(t *testing.T) { + d := testSetup(t) + input := randomExecuteReport(t, d) + c, err := codec.NewCodec(execCodecConfig) + require.NoError(t, err) + + result, err := c.Encode(testutils.Context(t), input, "ExecPluginReport") + require.NoError(t, err) + require.NotNil(t, result) + output := cciptypes.ExecutePluginReport{} + err = c.Decode(testutils.Context(t), result, &output, "ExecPluginReport") + require.NoError(t, err) + require.Equal(t, input, output) +} + +func TestExecutePluginCodecV2(t *testing.T) { + d := testSetup(t) + + testCases := []struct { + name string + report func(report cciptypes.ExecutePluginReport) cciptypes.ExecutePluginReport + expErr bool + }{ + { + name: "base report", + report: func(report cciptypes.ExecutePluginReport) cciptypes.ExecutePluginReport { return report }, + expErr: false, + }, + { + name: "reports have empty msgs", + report: func(report cciptypes.ExecutePluginReport) cciptypes.ExecutePluginReport { + report.ChainReports[0].Messages = []cciptypes.Message{} + report.ChainReports[4].Messages = []cciptypes.Message{} + return report + }, + expErr: false, + }, + { + name: "reports have empty offchain token data", + report: func(report cciptypes.ExecutePluginReport) cciptypes.ExecutePluginReport { + report.ChainReports[0].OffchainTokenData = [][][]byte{} + report.ChainReports[4].OffchainTokenData[1] = [][]byte{} + return report + }, + expErr: false, + }, + } + + ctx := testutils.Context(t) + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + cd, err := NewExecutePluginCodecV2() + require.NoError(t, err) + report := tc.report(randomExecuteReport(t, d)) + bytes, err := cd.Encode(ctx, report) + if tc.expErr { + assert.Error(t, err) + return + } + require.NoError(t, err) + + testSetup(t) + + // ignore msg hash in comparison + for i := range report.ChainReports { + for j := range report.ChainReports[i].Messages { + report.ChainReports[i].Messages[j].Header.MsgHash = cciptypes.Bytes32{} + report.ChainReports[i].Messages[j].Header.OnRamp = cciptypes.UnknownAddress{} + report.ChainReports[i].Messages[j].FeeToken = cciptypes.UnknownAddress{} + report.ChainReports[i].Messages[j].ExtraArgs = cciptypes.Bytes{} + report.ChainReports[i].Messages[j].FeeTokenAmount = cciptypes.BigInt{} + } + } + + // decode using the codec + codecDecoded, err := cd.Decode(ctx, bytes) + require.NoError(t, err) + assert.Equal(t, report, codecDecoded) + }) + } +} diff --git a/core/services/relay/evm/codec/codec_test.go b/core/services/relay/evm/codec/codec_test.go index 2da88abaac1..3136dc1c34e 100644 --- a/core/services/relay/evm/codec/codec_test.go +++ b/core/services/relay/evm/codec/codec_test.go @@ -3,12 +3,15 @@ package codec_test import ( "encoding/json" "math/big" + "math/rand" "testing" "github.com/cometbft/cometbft/libs/strings" "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" + cciptypes "github.com/smartcontractkit/chainlink-ccip/pkg/types/ccipocr3" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/utils" ocr2types "github.com/smartcontractkit/libocr/offchainreporting2plus/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -172,6 +175,72 @@ func TestCodec_EncodeTupleWithLists(t *testing.T) { require.Equal(t, expected, hexutil.Encode(result)[2:]) } +func TestCodec_CommitReport(t *testing.T) { + input := cciptypes.CommitPluginReport{ + MerkleRoots: []cciptypes.MerkleRootChain{ + { + ChainSel: cciptypes.ChainSelector(rand.Uint64()), + OnRampAddress: common.LeftPadBytes(utils.RandomAddress().Bytes(), 32), + SeqNumsRange: cciptypes.NewSeqNumRange( + cciptypes.SeqNum(rand.Uint64()), + cciptypes.SeqNum(rand.Uint64()), + ), + MerkleRoot: utils.RandomBytes32(), + }, + { + OnRampAddress: common.LeftPadBytes(utils.RandomAddress().Bytes(), 32), + ChainSel: cciptypes.ChainSelector(rand.Uint64()), + SeqNumsRange: cciptypes.NewSeqNumRange( + cciptypes.SeqNum(rand.Uint64()), + cciptypes.SeqNum(rand.Uint64()), + ), + MerkleRoot: utils.RandomBytes32(), + }, + }, + PriceUpdates: cciptypes.PriceUpdates{ + TokenPriceUpdates: []cciptypes.TokenPrice{ + { + TokenID: cciptypes.UnknownEncodedAddress(utils.RandomAddress().String()), + Price: cciptypes.NewBigInt(utils.RandUint256()), + }, + }, + GasPriceUpdates: []cciptypes.GasPriceChain{ + {GasPrice: cciptypes.NewBigInt(utils.RandUint256()), ChainSel: cciptypes.ChainSelector(rand.Uint64())}, + {GasPrice: cciptypes.NewBigInt(utils.RandUint256()), ChainSel: cciptypes.ChainSelector(rand.Uint64())}, + {GasPrice: cciptypes.NewBigInt(utils.RandUint256()), ChainSel: cciptypes.ChainSelector(rand.Uint64())}, + }, + }, + RMNSignatures: []cciptypes.RMNECDSASignature{ + {R: utils.RandomBytes32(), S: utils.RandomBytes32()}, + {R: utils.RandomBytes32(), S: utils.RandomBytes32()}, + }, + } + config := `[{"components":[{"name":"chainSel","type":"uint64","internalType":"uint64"},{"name":"onRampAddress","type":"bytes","internalType":"bytes"},{"name":"seqNumsRange","type":"uint64[2]","internalType":"uint64[2]"},{"name":"merkleRoot","type":"bytes32","internalType":"bytes32"}],"name":"merkleRoots","type":"tuple[]","internalType":"struct MerkleRootChain[]"},{"components":[{"components":[{"name":"tokenID","type":"string","internalType":"string"},{"name":"price","type":"uint256","internalType":"uint256"}],"name":"tokenPriceUpdates","type":"tuple[]","internalType":"struct TokenPrice[]"},{"components":[{"name":"chainSel","type":"uint64","internalType":"uint64"},{"name":"gasPrice","type":"uint256","internalType":"uint256"}],"name":"gasPriceUpdates","type":"tuple[]","internalType":"struct GasPriceChain[]"}],"name":"priceUpdates","type":"tuple","internalType":"struct PriceUpdates"},{"components":[{"name":"r","type":"bytes32","internalType":"bytes32"},{"name":"s","type":"bytes32","internalType":"bytes32"}],"name":"rmnSignatures","type":"tuple[]"}]` + codecConfig := types.CodecConfig{ + Configs: map[string]types.ChainCodecConfig{ + "CommitPluginReport": { + TypeABI: config, + ModifierConfigs: commoncodec.ModifiersConfig{ + &commoncodec.WrapperModifierConfig{Fields: map[string]string{ + "PriceUpdates.TokenPriceUpdates.Price": "Int", + "PriceUpdates.GasPriceUpdates.GasPrice": "Int", + }}, + }, + }, + }, + } + c, err := codec.NewCodec(codecConfig) + require.NoError(t, err) + + result, err := c.Encode(testutils.Context(t), input, "CommitPluginReport") + require.NoError(t, err) + require.NotNil(t, result) + output := cciptypes.CommitPluginReport{} + err = c.Decode(testutils.Context(t), result, &output, "CommitPluginReport") + require.NoError(t, err) + require.Equal(t, input, output) +} + type codecInterfaceTester struct { TestSelectionSupport }