From 786829f2fff78400faddd388dac1abc12588b060 Mon Sep 17 00:00:00 2001 From: Awbrey Hughlett Date: Fri, 15 Mar 2024 13:36:49 -0500 Subject: [PATCH] BCF-3015 Add Configuration for Encoding (#616) Solana allows multiple encoding types for account data. This commit adds support for two encoding types `borsh` and `bincode`. --- pkg/solana/chainreader/chain_reader.go | 2 +- pkg/solana/chainreader/chain_reader_test.go | 28 ++--- pkg/solana/codec/solana.go | 5 +- pkg/solana/codec/solana_test.go | 5 +- pkg/solana/config/chain_reader.go | 74 +++++++++-- pkg/solana/config/chain_reader_test.go | 118 ++++++++++++++++++ .../config/testChainReader_invalid.json | 15 +++ pkg/solana/config/testChainReader_valid.json | 37 ++++++ 8 files changed, 252 insertions(+), 32 deletions(-) create mode 100644 pkg/solana/config/chain_reader_test.go create mode 100644 pkg/solana/config/testChainReader_invalid.json create mode 100644 pkg/solana/config/testChainReader_valid.json diff --git a/pkg/solana/chainreader/chain_reader.go b/pkg/solana/chainreader/chain_reader.go index 3418b59ff..97be00f50 100644 --- a/pkg/solana/chainreader/chain_reader.go +++ b/pkg/solana/chainreader/chain_reader.go @@ -120,7 +120,7 @@ func (s *SolanaChainReaderService) init(namespaces map[string]config.ChainReader return err } - idlCodec, err := codec.NewIDLCodec(idl) + idlCodec, err := codec.NewIDLCodec(idl, config.BuilderForEncoding(method.Encoding)) if err != nil { return err } diff --git a/pkg/solana/chainreader/chain_reader_test.go b/pkg/solana/chainreader/chain_reader_test.go index 2daa5fc12..11caaaa0e 100644 --- a/pkg/solana/chainreader/chain_reader_test.go +++ b/pkg/solana/chainreader/chain_reader_test.go @@ -17,6 +17,7 @@ import ( codeccommon "github.com/smartcontractkit/chainlink-common/pkg/codec" "github.com/smartcontractkit/chainlink-common/pkg/codec/encodings" + "github.com/smartcontractkit/chainlink-common/pkg/codec/encodings/binary" "github.com/smartcontractkit/chainlink-common/pkg/logger" commontestutils "github.com/smartcontractkit/chainlink-common/pkg/loop/testutils" "github.com/smartcontractkit/chainlink-common/pkg/types" @@ -239,7 +240,7 @@ func newTestIDLAndCodec(t *testing.T) (string, codec.IDL, encodings.CodecFromTyp t.FailNow() } - entry, err := codec.NewIDLCodec(idl) + entry, err := codec.NewIDLCodec(idl, binary.LittleEndian()) if err != nil { t.Logf("failed to create new codec from test IDL: %s", err.Error()) t.FailNow() @@ -263,7 +264,6 @@ func newTestConfAndCodec(t *testing.T) (encodings.CodecFromTypeCodec, config.Cha Procedures: []config.ChainReaderProcedure{ { IDLAccount: testutils.TestStructWithNestedStruct, - Type: config.ProcedureTypeAnchor, OutputModifications: codeccommon.ModifiersConfig{ &codeccommon.RenameModifierConfig{Fields: map[string]string{"Value": "V"}}, }, @@ -337,19 +337,19 @@ func (r *chainReaderInterfaceTester) Setup(t *testing.T) { Methods: map[string]config.ChainDataReader{ MethodTakingLatestParamsReturningTestStruct: { AnchorIDL: fmt.Sprintf(baseIDL, testStructIDL, strings.Join([]string{midLevelStructIDL, innerStructIDL}, ",")), + Encoding: config.EncodingTypeBorsh, Procedures: []config.ChainReaderProcedure{ { IDLAccount: "TestStruct", - Type: config.ProcedureTypeAnchor, }, }, }, MethodReturningUint64: { AnchorIDL: fmt.Sprintf(baseIDL, uint64BaseTypeIDL, ""), + Encoding: config.EncodingTypeBorsh, Procedures: []config.ChainReaderProcedure{ { IDLAccount: "SimpleUint64Value", - Type: config.ProcedureTypeAnchor, OutputModifications: codeccommon.ModifiersConfig{ &codeccommon.PropertyExtractorConfig{FieldName: "I"}, }, @@ -358,10 +358,10 @@ func (r *chainReaderInterfaceTester) Setup(t *testing.T) { }, DifferentMethodReturningUint64: { AnchorIDL: fmt.Sprintf(baseIDL, uint64BaseTypeIDL, ""), + Encoding: config.EncodingTypeBorsh, Procedures: []config.ChainReaderProcedure{ { IDLAccount: "SimpleUint64Value", - Type: config.ProcedureTypeAnchor, OutputModifications: codeccommon.ModifiersConfig{ &codeccommon.PropertyExtractorConfig{FieldName: "I"}, }, @@ -370,10 +370,10 @@ func (r *chainReaderInterfaceTester) Setup(t *testing.T) { }, MethodReturningUint64Slice: { AnchorIDL: fmt.Sprintf(baseIDL, uint64SliceBaseTypeIDL, ""), + Encoding: config.EncodingTypeBincode, Procedures: []config.ChainReaderProcedure{ { IDLAccount: "Uint64Slice", - Type: config.ProcedureTypeAnchor, OutputModifications: codeccommon.ModifiersConfig{ &codeccommon.PropertyExtractorConfig{FieldName: "Vals"}, }, @@ -382,10 +382,10 @@ func (r *chainReaderInterfaceTester) Setup(t *testing.T) { }, MethodReturningSeenStruct: { AnchorIDL: fmt.Sprintf(baseIDL, testStructIDL, strings.Join([]string{midLevelStructIDL, innerStructIDL}, ",")), + Encoding: config.EncodingTypeBorsh, Procedures: []config.ChainReaderProcedure{ { IDLAccount: "TestStruct", - Type: config.ProcedureTypeAnchor, OutputModifications: codeccommon.ModifiersConfig{ &codeccommon.HardCodeModifierConfig{OffChainValues: map[string]any{"ExtraField": AnyExtraValue}}, // &codeccommon.RenameModifierConfig{Fields: map[string]string{"NestedStruct.Inner.IntVal": "I"}}, @@ -399,10 +399,10 @@ func (r *chainReaderInterfaceTester) Setup(t *testing.T) { Methods: map[string]config.ChainDataReader{ MethodReturningUint64: { AnchorIDL: fmt.Sprintf(baseIDL, uint64BaseTypeIDL, ""), + Encoding: config.EncodingTypeBorsh, Procedures: []config.ChainReaderProcedure{ { IDLAccount: "SimpleUint64Value", - Type: config.ProcedureTypeAnchor, OutputModifications: codeccommon.ModifiersConfig{ &codeccommon.PropertyExtractorConfig{FieldName: "I"}, }, @@ -451,7 +451,7 @@ func (r *wrappedTestChainReader) GetLatestValue(ctx context.Context, contractNam // returning the expected error to satisfy the test return types.ErrNotFound case AnyContractName + MethodReturningUint64: - cdc := makeTestCodec(r.test, fmt.Sprintf(baseIDL, uint64BaseTypeIDL, "")) + cdc := makeTestCodec(r.test, fmt.Sprintf(baseIDL, uint64BaseTypeIDL, ""), config.EncodingTypeBorsh) onChainStruct := struct { I uint64 }{ @@ -466,7 +466,7 @@ func (r *wrappedTestChainReader) GetLatestValue(ctx context.Context, contractNam r.client.On("ReadAll", mock.Anything, mock.Anything).Return(bts, nil).Once() case AnyContractName + MethodReturningUint64Slice: - cdc := makeTestCodec(r.test, fmt.Sprintf(baseIDL, uint64SliceBaseTypeIDL, "")) + cdc := makeTestCodec(r.test, fmt.Sprintf(baseIDL, uint64SliceBaseTypeIDL, ""), config.EncodingTypeBincode) onChainStruct := struct { Vals []uint64 }{ @@ -480,7 +480,7 @@ func (r *wrappedTestChainReader) GetLatestValue(ctx context.Context, contractNam r.client.On("ReadAll", mock.Anything, mock.Anything).Return(bts, nil).Once() case AnySecondContractName + MethodReturningUint64, AnyContractName + DifferentMethodReturningUint64: - cdc := makeTestCodec(r.test, fmt.Sprintf(baseIDL, uint64BaseTypeIDL, "")) + cdc := makeTestCodec(r.test, fmt.Sprintf(baseIDL, uint64BaseTypeIDL, ""), config.EncodingTypeBorsh) onChainStruct := struct { I uint64 }{ @@ -506,7 +506,7 @@ func (r *wrappedTestChainReader) GetLatestValue(ctx context.Context, contractNam nextTestStruct := r.testStructQueue[0] r.testStructQueue = r.testStructQueue[1:len(r.testStructQueue)] - cdc := makeTestCodec(r.test, fmt.Sprintf(baseIDL, testStructIDL, strings.Join([]string{midLevelStructIDL, innerStructIDL}, ","))) + cdc := makeTestCodec(r.test, fmt.Sprintf(baseIDL, testStructIDL, strings.Join([]string{midLevelStructIDL, innerStructIDL}, ",")), config.EncodingTypeBorsh) bts, err := cdc.Encode(ctx, nextTestStruct, "TestStruct") if err != nil { r.test.FailNow() @@ -575,7 +575,7 @@ func (r *chainReaderInterfaceTester) MaxWaitTimeForEvents() time.Duration { return maxWaitTime } -func makeTestCodec(t *testing.T, rawIDL string) encodings.CodecFromTypeCodec { +func makeTestCodec(t *testing.T, rawIDL string, encoding config.EncodingType) encodings.CodecFromTypeCodec { t.Helper() var idl codec.IDL @@ -584,7 +584,7 @@ func makeTestCodec(t *testing.T, rawIDL string) encodings.CodecFromTypeCodec { t.FailNow() } - testCodec, err := codec.NewIDLCodec(idl) + testCodec, err := codec.NewIDLCodec(idl, config.BuilderForEncoding(encoding)) if err != nil { t.Logf("failed to create new codec from test IDL: %s", err.Error()) t.FailNow() diff --git a/pkg/solana/codec/solana.go b/pkg/solana/codec/solana.go index 2df40529d..aecdbf612 100644 --- a/pkg/solana/codec/solana.go +++ b/pkg/solana/codec/solana.go @@ -29,7 +29,6 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/codec" "github.com/smartcontractkit/chainlink-common/pkg/codec/encodings" - "github.com/smartcontractkit/chainlink-common/pkg/codec/encodings/binary" "github.com/smartcontractkit/chainlink-common/pkg/types" ) @@ -62,11 +61,11 @@ func NewNamedModifierCodec(original types.RemoteCodec, itemType string, modifier } // NewIDLCodec is for Anchor custom types -func NewIDLCodec(idl IDL) (encodings.CodecFromTypeCodec, error) { +func NewIDLCodec(idl IDL, builder encodings.Builder) (encodings.CodecFromTypeCodec, error) { accounts := make(map[string]encodings.TypeCodec) refs := &codecRefs{ - builder: binary.LittleEndian(), + builder: builder, codecs: make(map[string]encodings.TypeCodec), typeDefs: idl.Types, dependencies: make(map[string][]string), diff --git a/pkg/solana/codec/solana_test.go b/pkg/solana/codec/solana_test.go index 48415dd07..73bcf28d8 100644 --- a/pkg/solana/codec/solana_test.go +++ b/pkg/solana/codec/solana_test.go @@ -11,6 +11,7 @@ import ( codeccommon "github.com/smartcontractkit/chainlink-common/pkg/codec" "github.com/smartcontractkit/chainlink-common/pkg/codec/encodings" + "github.com/smartcontractkit/chainlink-common/pkg/codec/encodings/binary" "github.com/smartcontractkit/chainlink-common/pkg/types" "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" @@ -113,7 +114,7 @@ func TestNewIDLCodec_CircularDependency(t *testing.T) { t.FailNow() } - _, err := codec.NewIDLCodec(idl) + _, err := codec.NewIDLCodec(idl, binary.LittleEndian()) assert.ErrorIs(t, err, types.ErrInvalidConfig) } @@ -127,7 +128,7 @@ func newTestIDLAndCodec(t *testing.T) (string, codec.IDL, encodings.CodecFromTyp t.FailNow() } - entry, err := codec.NewIDLCodec(idl) + entry, err := codec.NewIDLCodec(idl, binary.LittleEndian()) if err != nil { t.Logf("failed to create new codec from test IDL: %s", err.Error()) t.FailNow() diff --git a/pkg/solana/config/chain_reader.go b/pkg/solana/config/chain_reader.go index 980f6c535..f770c0939 100644 --- a/pkg/solana/config/chain_reader.go +++ b/pkg/solana/config/chain_reader.go @@ -1,6 +1,14 @@ package config -import "github.com/smartcontractkit/chainlink-common/pkg/codec" +import ( + "encoding/json" + "fmt" + + "github.com/smartcontractkit/chainlink-common/pkg/codec" + "github.com/smartcontractkit/chainlink-common/pkg/codec/encodings" + "github.com/smartcontractkit/chainlink-common/pkg/codec/encodings/binary" + "github.com/smartcontractkit/chainlink-common/pkg/types" +) type ChainReader struct { Namespaces map[string]ChainReaderMethods `json:"namespaces" toml:"namespaces"` @@ -11,29 +19,71 @@ type ChainReaderMethods struct { } type ChainDataReader struct { - AnchorIDL string `json:"anchorIDL" toml:"anchorIDL"` + AnchorIDL string `json:"anchorIDL" toml:"anchorIDL"` + // Encoding defines the type of encoding used for on-chain data. Currently supported + // are 'borsh' and 'bincode'. + Encoding EncodingType `json:"encoding" toml:"encoding"` Procedures []ChainReaderProcedure `json:"procedures" toml:"procedures"` } -type ProcedureType int +type EncodingType int const ( - ProcedureTypeInternal ProcedureType = iota - ProcedureTypeAnchor + EncodingTypeBorsh EncodingType = iota + EncodingTypeBincode + + encodingTypeBorshStr = "borsh" + encodingTypeBincodeStr = "bincode" ) +func (t EncodingType) MarshalJSON() ([]byte, error) { + switch t { + case EncodingTypeBorsh: + return json.Marshal(encodingTypeBorshStr) + case EncodingTypeBincode: + return json.Marshal(encodingTypeBincodeStr) + default: + return nil, fmt.Errorf("%w: unrecognized encoding type: %d", types.ErrInvalidConfig, t) + } +} + +func (t *EncodingType) UnmarshalJSON(data []byte) error { + var str string + + if err := json.Unmarshal(data, &str); err != nil { + return fmt.Errorf("%w: %s", types.ErrInvalidConfig, err.Error()) + } + + switch str { + case encodingTypeBorshStr: + *t = EncodingTypeBorsh + case encodingTypeBincodeStr: + *t = EncodingTypeBincode + default: + return fmt.Errorf("%w: unrecognized encoding type: %s", types.ErrInvalidConfig, str) + } + + return nil +} + type ChainReaderProcedure chainDataProcedureFields type chainDataProcedureFields struct { // IDLAccount refers to the account defined in the IDL. - IDLAccount string `json:"idlAccount"` - // Type describes the procedure type to use such as internal for static values, - // anchor-read for using an anchor generated IDL to read values from an account, - // or custom structure for reading from a native account. Currently, only anchor - // reads are supported, but the type is a placeholder to allow internal functions - // to be run apart from anchor reads. - Type ProcedureType `json:"type"` + IDLAccount string `json:"idlAccount,omitempty"` // OutputModifications provides modifiers to convert chain data format to custom // output formats. OutputModifications codec.ModifiersConfig `json:"outputModifications,omitempty"` } + +// BuilderForEncoding returns a builder for the encoding configuration. Defaults to little endian. +func BuilderForEncoding(eType EncodingType) encodings.Builder { + switch eType { + case EncodingTypeBorsh: + return binary.LittleEndian() + case EncodingTypeBincode: + return binary.BigEndian() + default: + return binary.LittleEndian() + } +} diff --git a/pkg/solana/config/chain_reader_test.go b/pkg/solana/config/chain_reader_test.go new file mode 100644 index 000000000..26ac5ef91 --- /dev/null +++ b/pkg/solana/config/chain_reader_test.go @@ -0,0 +1,118 @@ +package config_test + +import ( + _ "embed" + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + codeccommon "github.com/smartcontractkit/chainlink-common/pkg/codec" + "github.com/smartcontractkit/chainlink-common/pkg/codec/encodings/binary" + "github.com/smartcontractkit/chainlink-common/pkg/types" + + "github.com/smartcontractkit/chainlink-solana/pkg/solana/codec/testutils" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/config" +) + +//go:embed testChainReader_valid.json +var validJSON string + +//go:embed testChainReader_invalid.json +var invalidJSON string + +func TestChainReaderConfig(t *testing.T) { + t.Parallel() + + t.Run("valid unmarshal", func(t *testing.T) { + t.Parallel() + + var result config.ChainReader + require.NoError(t, json.Unmarshal([]byte(validJSON), &result)) + assert.Equal(t, validChainReaderConfig, result) + }) + + t.Run("invalid unmarshal", func(t *testing.T) { + t.Parallel() + + var result config.ChainReader + require.ErrorIs(t, json.Unmarshal([]byte(invalidJSON), &result), types.ErrInvalidConfig) + }) + + t.Run("marshal", func(t *testing.T) { + t.Parallel() + + result, err := json.Marshal(validChainReaderConfig) + + require.NoError(t, err) + + var conf config.ChainReader + + require.NoError(t, json.Unmarshal(result, &conf)) + assert.Equal(t, validChainReaderConfig, conf) + }) +} + +func TestEncodingType_Fail(t *testing.T) { + t.Parallel() + + _, err := json.Marshal(config.EncodingType(100)) + + require.NotNil(t, err) + + var tp config.EncodingType + + require.ErrorIs(t, json.Unmarshal([]byte(`42`), &tp), types.ErrInvalidConfig) + require.ErrorIs(t, json.Unmarshal([]byte(`"invalid"`), &tp), types.ErrInvalidConfig) +} + +func TestBuilderForEncoding_Default(t *testing.T) { + t.Parallel() + + builder := config.BuilderForEncoding(config.EncodingType(100)) + require.Equal(t, binary.LittleEndian(), builder) +} + +var validChainReaderConfig = config.ChainReader{ + Namespaces: map[string]config.ChainReaderMethods{ + "Contract": { + Methods: map[string]config.ChainDataReader{ + "Method": { + AnchorIDL: "test idl 1", + Encoding: config.EncodingTypeBorsh, + Procedures: []config.ChainReaderProcedure{ + { + IDLAccount: testutils.TestStructWithNestedStruct, + }, + }, + }, + "MethodWithOpts": { + AnchorIDL: "test idl 2", + Encoding: config.EncodingTypeBorsh, + Procedures: []config.ChainReaderProcedure{ + { + IDLAccount: testutils.TestStructWithNestedStruct, + OutputModifications: codeccommon.ModifiersConfig{ + &codeccommon.PropertyExtractorConfig{FieldName: "DurationVal"}, + }, + }, + }, + }, + }, + }, + "OtherContract": { + Methods: map[string]config.ChainDataReader{ + "Method": { + AnchorIDL: "test idl 3", + Encoding: config.EncodingTypeBincode, + Procedures: []config.ChainReaderProcedure{ + { + IDLAccount: testutils.TestStructWithNestedStruct, + }, + }, + }, + }, + }, + }, +} diff --git a/pkg/solana/config/testChainReader_invalid.json b/pkg/solana/config/testChainReader_invalid.json new file mode 100644 index 000000000..b428b6115 --- /dev/null +++ b/pkg/solana/config/testChainReader_invalid.json @@ -0,0 +1,15 @@ +{ + "namespaces": { + "Contract": { + "methods": { + "Method": { + "anchorIDL": "test idl 1", + "encoding": "invalid", + "procedures": [{ + "idlAccount": "StructWithNestedStruct" + }] + } + } + } + } +} \ No newline at end of file diff --git a/pkg/solana/config/testChainReader_valid.json b/pkg/solana/config/testChainReader_valid.json new file mode 100644 index 000000000..d2649739d --- /dev/null +++ b/pkg/solana/config/testChainReader_valid.json @@ -0,0 +1,37 @@ +{ + "namespaces": { + "Contract": { + "methods": { + "Method": { + "anchorIDL": "test idl 1", + "encoding": "borsh", + "procedures": [{ + "idlAccount": "StructWithNestedStruct" + }] + }, + "MethodWithOpts": { + "anchorIDL": "test idl 2", + "encoding": "borsh", + "procedures": [{ + "idlAccount": "StructWithNestedStruct", + "outputModifications": [{ + "Type": "extract property", + "FieldName": "DurationVal" + }] + }] + } + } + }, + "OtherContract": { + "methods": { + "Method": { + "anchorIDL": "test idl 3", + "encoding": "bincode", + "procedures": [{ + "idlAccount": "StructWithNestedStruct" + }] + } + } + } + } +} \ No newline at end of file