diff --git a/integration-tests/relayinterface/chain_components_test.go b/integration-tests/relayinterface/chain_components_test.go index cd47d224e..7512be361 100644 --- a/integration-tests/relayinterface/chain_components_test.go +++ b/integration-tests/relayinterface/chain_components_test.go @@ -6,6 +6,7 @@ package relayinterface import ( "context" "encoding/binary" + "encoding/json" "io" "os" "path/filepath" @@ -19,7 +20,7 @@ import ( "github.com/gagliardetto/solana-go/text" "github.com/stretchr/testify/require" - "github.com/smartcontractkit/chainlink-common/pkg/codec" + commoncodec "github.com/smartcontractkit/chainlink-common/pkg/codec" "github.com/smartcontractkit/chainlink-common/pkg/logger" commontestutils "github.com/smartcontractkit/chainlink-common/pkg/loop/testutils" "github.com/smartcontractkit/chainlink-common/pkg/types" @@ -27,6 +28,8 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/types/query/primitives" "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/codec" + contract "github.com/smartcontractkit/chainlink-solana/contracts/generated/contract_reader_interface" "github.com/smartcontractkit/chainlink-solana/integration-tests/solclient" "github.com/smartcontractkit/chainlink-solana/integration-tests/utils" @@ -105,7 +108,7 @@ func RunChainComponentsInLoopSolanaTests[T TestingT[T]](t T, it ChainComponentsI func RunContractReaderSolanaTests[T TestingT[T]](t T, it *SolanaChainComponentsInterfaceTester[T]) { RunContractReaderInterfaceTests(t, it, false, true) - testCases := []Testcase[T]{} + var testCases []Testcase[T] RunTests(t, it, testCases) } @@ -113,7 +116,7 @@ func RunContractReaderSolanaTests[T TestingT[T]](t T, it *SolanaChainComponentsI func RunContractReaderInLoopTests[T TestingT[T]](t T, it ChainComponentsInterfaceTester[T]) { RunContractReaderInterfaceTests(t, it, false, true) - testCases := []Testcase[T]{} + var testCases []Testcase[T] RunTests(t, it, testCases) } @@ -129,50 +132,41 @@ type SolanaChainComponentsInterfaceTesterHelper[T TestingT[T]] interface { type SolanaChainComponentsInterfaceTester[T TestingT[T]] struct { TestSelectionSupport - Helper SolanaChainComponentsInterfaceTesterHelper[T] - cr *chainreader.SolanaChainReaderService - chainReaderConfig config.ChainReader + Helper SolanaChainComponentsInterfaceTesterHelper[T] + cr *chainreader.SolanaChainReaderService + contractReaderConfig config.ContractReader } func (it *SolanaChainComponentsInterfaceTester[T]) Setup(t T) { t.Cleanup(func() {}) - it.chainReaderConfig = config.ChainReader{ - Namespaces: map[string]config.ChainReaderMethods{ + it.contractReaderConfig = config.ContractReader{ + Namespaces: map[string]config.ChainContractReader{ AnyContractName: { - Methods: map[string]config.ChainDataReader{ + IDL: mustUnmarshalIDL(t, string(it.Helper.GetJSONEncodedIDL(t))), + Reads: map[string]config.ReadDefinition{ MethodReturningUint64: { - AnchorIDL: string(it.Helper.GetJSONEncodedIDL(t)), - Encoding: config.EncodingTypeBorsh, - Procedure: config.ChainReaderProcedure{ - IDLAccount: "DataAccount", - OutputModifications: codec.ModifiersConfig{ - &codec.PropertyExtractorConfig{FieldName: "U64Value"}, - }, + ChainSpecificName: "DataAccount", + ReadType: config.Account, + OutputModifications: commoncodec.ModifiersConfig{ + &commoncodec.PropertyExtractorConfig{FieldName: "U64Value"}, }, }, MethodReturningUint64Slice: { - AnchorIDL: string(it.Helper.GetJSONEncodedIDL(t)), - Encoding: config.EncodingTypeBorsh, - Procedure: config.ChainReaderProcedure{ - IDLAccount: "DataAccount", - OutputModifications: codec.ModifiersConfig{ - &codec.PropertyExtractorConfig{FieldName: "U64Slice"}, - }, + ChainSpecificName: "DataAccount", + OutputModifications: commoncodec.ModifiersConfig{ + &commoncodec.PropertyExtractorConfig{FieldName: "U64Slice"}, }, }, }, }, AnySecondContractName: { - Methods: map[string]config.ChainDataReader{ + IDL: mustUnmarshalIDL(t, string(it.Helper.GetJSONEncodedIDL(t))), + Reads: map[string]config.ReadDefinition{ MethodReturningUint64: { - AnchorIDL: string(it.Helper.GetJSONEncodedIDL(t)), - Encoding: config.EncodingTypeBorsh, - Procedure: config.ChainReaderProcedure{ - IDLAccount: "DataAccount", - OutputModifications: codec.ModifiersConfig{ - &codec.PropertyExtractorConfig{FieldName: "U64Value"}, - }, + ChainSpecificName: "DataAccount", + OutputModifications: commoncodec.ModifiersConfig{ + &commoncodec.PropertyExtractorConfig{FieldName: "U64Value"}, }, }, }, @@ -199,7 +193,7 @@ func (it *SolanaChainComponentsInterfaceTester[T]) GetContractReader(t T) types. return it.cr } - svc, err := chainreader.NewChainReaderService(it.Helper.Logger(t), it.Helper.RPCClient(), it.chainReaderConfig) + svc, err := chainreader.NewChainReaderService(it.Helper.Logger(t), it.Helper.RPCClient(), it.contractReaderConfig) require.NoError(t, err) require.NoError(t, svc.Start(ctx)) @@ -423,3 +417,13 @@ func setupTestValidator(t *testing.T, upgradeAuthority string) (string, string) return client.SetupLocalSolNodeWithFlags(t, flags...) } + +func mustUnmarshalIDL[T TestingT[T]](t T, rawIDL string) codec.IDL { + var idl codec.IDL + if err := json.Unmarshal([]byte(rawIDL), &idl); err != nil { + t.Errorf("failed to unmarshal test IDL", err) + t.FailNow() + } + + return idl +} diff --git a/pkg/solana/chainreader/account_read_binding.go b/pkg/solana/chainreader/account_read_binding.go index 71ebb131b..eacd45fad 100644 --- a/pkg/solana/chainreader/account_read_binding.go +++ b/pkg/solana/chainreader/account_read_binding.go @@ -4,30 +4,32 @@ import ( "context" "github.com/gagliardetto/solana-go" - "github.com/gagliardetto/solana-go/rpc" "github.com/smartcontractkit/chainlink-common/pkg/types" + + "github.com/smartcontractkit/chainlink-solana/pkg/solana/codec" ) -// accountReadBinding provides decoding and reading Solana Account data using a defined codec. The -// `idlAccount` refers to the account name in the IDL for which the codec has a type mapping. +// accountReadBinding provides decoding and reading Solana Account data using a defined codec. type accountReadBinding struct { - idlAccount string - codec types.RemoteCodec - key solana.PublicKey - opts *rpc.GetAccountInfoOpts + namespace, genericName string + codec types.RemoteCodec + key solana.PublicKey } -func newAccountReadBinding(acct string, codec types.RemoteCodec, opts *rpc.GetAccountInfoOpts) *accountReadBinding { +func newAccountReadBinding(namespace, genericName string) *accountReadBinding { return &accountReadBinding{ - idlAccount: acct, - codec: codec, - opts: opts, + namespace: namespace, + genericName: genericName, } } var _ readBinding = &accountReadBinding{} +func (b *accountReadBinding) SetCodec(codec types.RemoteCodec) { + b.codec = codec +} + func (b *accountReadBinding) SetAddress(key solana.PublicKey) { b.key = key } @@ -36,10 +38,10 @@ func (b *accountReadBinding) GetAddress() solana.PublicKey { return b.key } -func (b *accountReadBinding) CreateType(_ bool) (any, error) { - return b.codec.CreateType(b.idlAccount, false) +func (b *accountReadBinding) CreateType(forEncoding bool) (any, error) { + return b.codec.CreateType(codec.WrapItemType(forEncoding, b.namespace, b.genericName, codec.ChainConfigTypeAccountDef), forEncoding) } func (b *accountReadBinding) Decode(ctx context.Context, bts []byte, outVal any) error { - return b.codec.Decode(ctx, bts, outVal, b.idlAccount) + return b.codec.Decode(ctx, bts, outVal, codec.WrapItemType(false, b.namespace, b.genericName, codec.ChainConfigTypeAccountDef)) } diff --git a/pkg/solana/chainreader/batch.go b/pkg/solana/chainreader/batch.go index 43e4971b9..d5990601d 100644 --- a/pkg/solana/chainreader/batch.go +++ b/pkg/solana/chainreader/batch.go @@ -77,7 +77,6 @@ func doMethodBatchCall(ctx context.Context, client MultipleAccountGetter, bindin results[idx].err, binding.Decode(ctx, data[idx], results[idx].returnVal), ) - continue } diff --git a/pkg/solana/chainreader/bindings.go b/pkg/solana/chainreader/bindings.go index 51cc8980a..751a58fdd 100644 --- a/pkg/solana/chainreader/bindings.go +++ b/pkg/solana/chainreader/bindings.go @@ -12,6 +12,7 @@ import ( type readBinding interface { SetAddress(solana.PublicKey) GetAddress() solana.PublicKey + SetCodec(types.RemoteCodec) CreateType(bool) (any, error) Decode(context.Context, []byte, any) error } @@ -70,3 +71,11 @@ func (b namespaceBindings) Bind(binding types.BoundContract) error { return nil } + +func (b namespaceBindings) SetCodec(codec types.RemoteCodec) { + for _, nbs := range b { + for _, rb := range nbs { + rb.SetCodec(codec) + } + } +} diff --git a/pkg/solana/chainreader/bindings_test.go b/pkg/solana/chainreader/bindings_test.go index d8b510648..e8dbea89a 100644 --- a/pkg/solana/chainreader/bindings_test.go +++ b/pkg/solana/chainreader/bindings_test.go @@ -46,6 +46,8 @@ type mockBinding struct { mock.Mock } +func (_m *mockBinding) SetCodec(_ types.RemoteCodec) {} + func (_m *mockBinding) SetAddress(_ solana.PublicKey) {} func (_m *mockBinding) GetAddress() solana.PublicKey { diff --git a/pkg/solana/chainreader/chain_reader.go b/pkg/solana/chainreader/chain_reader.go index d017eb25d..1edcb9b8e 100644 --- a/pkg/solana/chainreader/chain_reader.go +++ b/pkg/solana/chainreader/chain_reader.go @@ -2,7 +2,6 @@ package chainreader import ( "context" - "encoding/json" "errors" "fmt" "sync" @@ -10,7 +9,7 @@ import ( "github.com/gagliardetto/solana-go" "github.com/gagliardetto/solana-go/rpc" - codeccommon "github.com/smartcontractkit/chainlink-common/pkg/codec" + commoncodec "github.com/smartcontractkit/chainlink-common/pkg/codec" "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink-common/pkg/services" "github.com/smartcontractkit/chainlink-common/pkg/types" @@ -33,6 +32,8 @@ type SolanaChainReaderService struct { // internal values bindings namespaceBindings lookup *lookup + parsed *codec.ParsedTypes + codec types.RemoteCodec // service state management wg sync.WaitGroup @@ -45,18 +46,27 @@ var ( ) // NewChainReaderService is a constructor for a new ChainReaderService for Solana. Returns a nil service on error. -func NewChainReaderService(lggr logger.Logger, dataReader MultipleAccountGetter, cfg config.ChainReader) (*SolanaChainReaderService, error) { +func NewChainReaderService(lggr logger.Logger, dataReader MultipleAccountGetter, cfg config.ContractReader) (*SolanaChainReaderService, error) { svc := &SolanaChainReaderService{ lggr: logger.Named(lggr, ServiceName), client: dataReader, bindings: namespaceBindings{}, lookup: newLookup(), + parsed: &codec.ParsedTypes{EncoderDefs: map[string]codec.Entry{}, DecoderDefs: map[string]codec.Entry{}}, } if err := svc.init(cfg.Namespaces); err != nil { return nil, err } + svcCodec, err := svc.parsed.ToCodec() + if err != nil { + return nil, err + } + + svc.codec = svcCodec + + svc.bindings.SetCodec(svcCodec) return svc, nil } @@ -106,15 +116,15 @@ func (s *SolanaChainReaderService) GetLatestValue(ctx context.Context, readIdent s.wg.Add(1) defer s.wg.Done() - vals, ok := s.lookup.getContractForReadIdentifiers(readIdentifier) + values, ok := s.lookup.getContractForReadIdentifiers(readIdentifier) if !ok { return fmt.Errorf("%w: no contract for read identifier %s", types.ErrInvalidType, readIdentifier) } batch := []call{ { - ContractName: vals.contract, - ReadName: vals.readName, + ContractName: values.contract, + ReadName: values.genericName, Params: params, ReturnVal: returnVal, }, @@ -217,78 +227,102 @@ func (s *SolanaChainReaderService) CreateContractType(readIdentifier string, for return nil, fmt.Errorf("%w: no contract for read identifier", types.ErrInvalidConfig) } - return s.bindings.CreateType(values.contract, values.readName, forEncoding) + return s.bindings.CreateType(values.contract, values.genericName, forEncoding) } -func (s *SolanaChainReaderService) init(namespaces map[string]config.ChainReaderMethods) error { - for namespace, methods := range namespaces { - for methodName, method := range methods.Methods { - var idl codec.IDL - if err := json.Unmarshal([]byte(method.AnchorIDL), &idl); err != nil { - return err - } - - idlCodec, err := codec.NewIDLAccountCodec(idl, config.BuilderForEncoding(method.Encoding)) - if err != nil { - return err - } - - s.lookup.addReadNameForContract(namespace, methodName) +func (s *SolanaChainReaderService) addCodecDef(forEncoding bool, namespace, genericName string, readType codec.ChainConfigType, idl codec.IDL, idlDefinition interface{}, modCfg commoncodec.ModifiersConfig) error { + mod, err := modCfg.ToModifier(codec.DecoderHooks...) + if err != nil { + return err + } - procedure := method.Procedure + cEntry, err := codec.CreateCodecEntry(idlDefinition, genericName, idl, mod) + if err != nil { + return err + } - injectAddressModifier(procedure.OutputModifications) + if forEncoding { + s.parsed.EncoderDefs[codec.WrapItemType(forEncoding, namespace, genericName, readType)] = cEntry + } else { + s.parsed.DecoderDefs[codec.WrapItemType(forEncoding, namespace, genericName, readType)] = cEntry + } + return nil +} - mod, err := procedure.OutputModifications.ToModifier(codec.DecoderHooks...) +func (s *SolanaChainReaderService) init(namespaces map[string]config.ChainContractReader) error { + for namespace, nameSpaceDef := range namespaces { + for genericName, read := range nameSpaceDef.Reads { + injectAddressModifier(read.InputModifications, read.OutputModifications) + idlDef, err := codec.FindDefinitionFromIDL(codec.ChainConfigTypeAccountDef, read.ChainSpecificName, nameSpaceDef.IDL) if err != nil { return err } - codecWithModifiers, err := codec.NewNamedModifierCodec(idlCodec, procedure.IDLAccount, mod) - if err != nil { - return err + switch read.ReadType { + case config.Account: + accountIDLDef, isOk := idlDef.(codec.IdlTypeDef) + if !isOk { + return fmt.Errorf("unexpected type %T from IDL definition for account read: %q, with chainSpecificName: %q, of type: %q", accountIDLDef, genericName, read.ChainSpecificName, read.ReadType) + } + if err = s.addAccountRead(namespace, genericName, nameSpaceDef.IDL, accountIDLDef, read); err != nil { + return err + } + case config.Event: + eventIDlDef, isOk := idlDef.(codec.IdlEvent) + if !isOk { + return fmt.Errorf("unexpected type %T from IDL definition for log read: %q, with chainSpecificName: %q, of type: %q", eventIDlDef, genericName, read.ChainSpecificName, read.ReadType) + } + // TODO s.addLogRead() + return fmt.Errorf("implement me") + default: + return fmt.Errorf("unexpected read type %q for: %q in namespace: %q", read.ReadType, genericName, namespace) } - - s.bindings.AddReadBinding(namespace, methodName, newAccountReadBinding( - procedure.IDLAccount, - codecWithModifiers, - createRPCOpts(procedure.RPCOpts), - )) } } return nil } -// injectAddressModifier injects AddressModifier into OutputModifications. -// This is necessary because AddressModifier cannot be serialized and must be applied at runtime. -func injectAddressModifier(outputModifications codeccommon.ModifiersConfig) { - for i, modConfig := range outputModifications { - if addrModifierConfig, ok := modConfig.(*codeccommon.AddressBytesToStringModifierConfig); ok { - addrModifierConfig.Modifier = codec.SolanaAddressModifier{} - outputModifications[i] = addrModifierConfig - } +func (s *SolanaChainReaderService) addAccountRead(namespace string, genericName string, idl codec.IDL, idlType codec.IdlTypeDef, readDefinition config.ReadDefinition) error { + inputAccountIDLDef := codec.NilIdlTypeDefTy + // TODO: + // if hasPDA{ + // inputAccountIDLDef = pdaType + // } + if err := s.addCodecDef(true, namespace, genericName, codec.ChainConfigTypeAccountDef, idl, inputAccountIDLDef, readDefinition.InputModifications); err != nil { + return err } -} -func createRPCOpts(opts *config.RPCOpts) *rpc.GetAccountInfoOpts { - if opts == nil { - return nil + if err := s.addCodecDef(false, namespace, genericName, codec.ChainConfigTypeAccountDef, idl, idlType, readDefinition.OutputModifications); err != nil { + return err } - result := &rpc.GetAccountInfoOpts{ - DataSlice: opts.DataSlice, - } + s.lookup.addReadNameForContract(namespace, genericName) - if opts.Encoding != nil { - result.Encoding = *opts.Encoding - } + s.bindings.AddReadBinding(namespace, genericName, newAccountReadBinding( + namespace, + genericName, + )) - if opts.Commitment != nil { - result.Commitment = *opts.Commitment + return nil +} + +// injectAddressModifier injects AddressModifier into OutputModifications. +// This is necessary because AddressModifier cannot be serialized and must be applied at runtime. +func injectAddressModifier(inputModifications, outputModifications commoncodec.ModifiersConfig) { + for i, modConfig := range inputModifications { + if addrModifierConfig, ok := modConfig.(*commoncodec.AddressBytesToStringModifierConfig); ok { + addrModifierConfig.Modifier = codec.SolanaAddressModifier{} + outputModifications[i] = addrModifierConfig + } } - return result + for i, modConfig := range outputModifications { + if addrModifierConfig, ok := modConfig.(*commoncodec.AddressBytesToStringModifierConfig); ok { + addrModifierConfig.Modifier = codec.SolanaAddressModifier{} + outputModifications[i] = addrModifierConfig + } + } } type accountDataReader struct { diff --git a/pkg/solana/chainreader/chain_reader_test.go b/pkg/solana/chainreader/chain_reader_test.go index 9f27ae0b7..de37567b6 100644 --- a/pkg/solana/chainreader/chain_reader_test.go +++ b/pkg/solana/chainreader/chain_reader_test.go @@ -14,7 +14,6 @@ import ( "github.com/gagliardetto/solana-go" ag_solana "github.com/gagliardetto/solana-go" - "github.com/gagliardetto/solana-go/rpc" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -64,7 +63,7 @@ func TestSolanaChainReaderService_ServiceCtx(t *testing.T) { t.Parallel() ctx := tests.Context(t) - svc, err := chainreader.NewChainReaderService(logger.Test(t), new(mockedRPCClient), config.ChainReader{}) + svc, err := chainreader.NewChainReaderService(logger.Test(t), new(mockedRPCClient), config.ContractReader{}) require.NoError(t, err) require.NotNil(t, svc) @@ -289,21 +288,19 @@ func newTestIDLAndCodec(t *testing.T) (string, codec.IDL, types.RemoteCodec) { return testutils.JSONIDLWithAllTypes, idl, entry } -func newTestConfAndCodec(t *testing.T) (types.RemoteCodec, config.ChainReader) { +func newTestConfAndCodec(t *testing.T) (types.RemoteCodec, config.ContractReader) { t.Helper() - rawIDL, _, testCodec := newTestIDLAndCodec(t) - conf := config.ChainReader{ - Namespaces: map[string]config.ChainReaderMethods{ + conf := config.ContractReader{ + Namespaces: map[string]config.ChainContractReader{ Namespace: { - Methods: map[string]config.ChainDataReader{ + IDL: mustUnmarshalIDL(t, rawIDL), + Reads: map[string]config.ReadDefinition{ NamedMethod: { - AnchorIDL: rawIDL, - Procedure: config.ChainReaderProcedure{ - IDLAccount: testutils.TestStructWithNestedStruct, - OutputModifications: codeccommon.ModifiersConfig{ - &codeccommon.RenameModifierConfig{Fields: map[string]string{"Value": "V"}}, - }, + ChainSpecificName: testutils.TestStructWithNestedStruct, + ReadType: config.Account, + OutputModifications: codeccommon.ModifiersConfig{ + &codeccommon.RenameModifierConfig{Fields: map[string]string{"Value": "V"}}, }, }, }, @@ -385,7 +382,7 @@ func (_m *mockedRPCClient) SetForAddress(pk ag_solana.PublicKey, bts []byte, err type chainReaderInterfaceTester struct { TestSelectionSupport - conf config.ChainReader + conf config.ContractReader address []string reader *wrappedTestChainReader } @@ -415,75 +412,49 @@ func (r *chainReaderInterfaceTester) Setup(t *testing.T) { r.address[idx] = ag_solana.NewWallet().PublicKey().String() } - encodingBase64 := solana.EncodingBase64 - commitment := rpc.CommitmentConfirmed - offset := uint64(1) - length := uint64(1) - - r.conf = config.ChainReader{ - Namespaces: map[string]config.ChainReaderMethods{ + r.conf = config.ContractReader{ + Namespaces: map[string]config.ChainContractReader{ AnyContractName: { - Methods: map[string]config.ChainDataReader{ + IDL: mustUnmarshalIDL(t, fullTestIDL(t)), + Reads: map[string]config.ReadDefinition{ MethodTakingLatestParamsReturningTestStruct: { - AnchorIDL: fullStructIDL(t), - Encoding: config.EncodingTypeBorsh, - Procedure: config.ChainReaderProcedure{ - IDLAccount: "TestStruct", - RPCOpts: &config.RPCOpts{ - Encoding: &encodingBase64, - Commitment: &commitment, - DataSlice: &rpc.DataSlice{ - Offset: &offset, - Length: &length, - }, - }, - }, + ReadType: config.Account, + ChainSpecificName: "TestStruct", }, MethodReturningUint64: { - AnchorIDL: fmt.Sprintf(baseIDL, uint64BaseTypeIDL, ""), - Encoding: config.EncodingTypeBorsh, - Procedure: config.ChainReaderProcedure{ - IDLAccount: "SimpleUint64Value", - OutputModifications: codeccommon.ModifiersConfig{ - &codeccommon.PropertyExtractorConfig{FieldName: "I"}, - }, + ReadType: config.Account, + ChainSpecificName: "SimpleUint64Value", + OutputModifications: codeccommon.ModifiersConfig{ + &codeccommon.PropertyExtractorConfig{FieldName: "I"}, }, }, MethodReturningUint64Slice: { - AnchorIDL: fmt.Sprintf(baseIDL, uint64SliceBaseTypeIDL, ""), - Encoding: config.EncodingTypeBincode, - Procedure: config.ChainReaderProcedure{ - IDLAccount: "Uint64Slice", - OutputModifications: codeccommon.ModifiersConfig{ - &codeccommon.PropertyExtractorConfig{FieldName: "Vals"}, - }, + ChainSpecificName: "Uint64Slice", + ReadType: config.Account, + OutputModifications: codeccommon.ModifiersConfig{ + &codeccommon.PropertyExtractorConfig{FieldName: "Vals"}, }, }, MethodReturningSeenStruct: { - AnchorIDL: fullStructIDL(t), - Encoding: config.EncodingTypeBorsh, - Procedure: config.ChainReaderProcedure{ - IDLAccount: "TestStruct", - OutputModifications: codeccommon.ModifiersConfig{ - &codeccommon.AddressBytesToStringModifierConfig{ - Fields: []string{"Accountstruct.Accountstr"}, - }, - &codeccommon.HardCodeModifierConfig{OffChainValues: map[string]any{"ExtraField": AnyExtraValue}}, + ChainSpecificName: "TestStruct", + ReadType: config.Account, + OutputModifications: codeccommon.ModifiersConfig{ + &codeccommon.AddressBytesToStringModifierConfig{ + Fields: []string{"AccountStruct.AccountStr"}, }, + &codeccommon.HardCodeModifierConfig{OffChainValues: map[string]any{"ExtraField": AnyExtraValue}}, }, }, }, }, AnySecondContractName: { - Methods: map[string]config.ChainDataReader{ + IDL: mustUnmarshalIDL(t, fmt.Sprintf(baseIDL, uint64BaseTypeIDL, "")), + Reads: map[string]config.ReadDefinition{ MethodReturningUint64: { - AnchorIDL: fmt.Sprintf(baseIDL, uint64BaseTypeIDL, ""), - Encoding: config.EncodingTypeBorsh, - Procedure: config.ChainReaderProcedure{ - IDLAccount: "SimpleUint64Value", - OutputModifications: codeccommon.ModifiersConfig{ - &codeccommon.PropertyExtractorConfig{FieldName: "I"}, - }, + ChainSpecificName: "SimpleUint64Value", + ReadType: config.Account, + OutputModifications: codeccommon.ModifiersConfig{ + &codeccommon.PropertyExtractorConfig{FieldName: "I"}, }, }, }, @@ -574,7 +545,7 @@ func (r *wrappedTestChainReader) GetLatestValue(ctx context.Context, readIdentif case AnyContractName + EventName: r.test.Skip("Events are not yet supported in Solana") case AnyContractName + MethodReturningUint64: - cdc := makeTestCodec(r.test, fmt.Sprintf(baseIDL, uint64BaseTypeIDL, ""), config.EncodingTypeBorsh) + cdc := makeTestCodec(r.test, fmt.Sprintf(baseIDL, uint64BaseTypeIDL, "")) onChainStruct := struct { I uint64 }{ @@ -587,7 +558,7 @@ func (r *wrappedTestChainReader) GetLatestValue(ctx context.Context, readIdentif r.test.FailNow() } case AnyContractName + MethodReturningUint64Slice: - cdc := makeTestCodec(r.test, fmt.Sprintf(baseIDL, uint64SliceBaseTypeIDL, ""), config.EncodingTypeBincode) + cdc := makeTestCodec(r.test, fmt.Sprintf(baseIDL, uint64SliceBaseTypeIDL, "")) onChainStruct := struct { Vals []uint64 }{ @@ -599,7 +570,7 @@ func (r *wrappedTestChainReader) GetLatestValue(ctx context.Context, readIdentif r.test.FailNow() } case AnySecondContractName + MethodReturningUint64, AnyContractName: - cdc := makeTestCodec(r.test, fmt.Sprintf(baseIDL, uint64BaseTypeIDL, ""), config.EncodingTypeBorsh) + cdc := makeTestCodec(r.test, fmt.Sprintf(baseIDL, uint64BaseTypeIDL, "")) onChainStruct := struct { I uint64 }{ @@ -624,7 +595,7 @@ func (r *wrappedTestChainReader) GetLatestValue(ctx context.Context, readIdentif r.testStructQueue = r.testStructQueue[1:len(r.testStructQueue)] // split into two encoded parts to test the preloading function - cdc := makeTestCodec(r.test, fullStructIDL(r.test), config.EncodingTypeBorsh) + cdc := makeTestCodec(r.test, fullStructIDL(r.test)) if strings.Contains(r.test.Name(), "wraps_config_with_modifiers_using_its_own_mapstructure_overrides") { // TODO: This is a temporary solution. We are manually retyping this struct to avoid breaking unrelated tests. @@ -760,16 +731,10 @@ func (r *chainReaderInterfaceTester) MaxWaitTimeForEvents() time.Duration { return maxWaitTime } -func makeTestCodec(t *testing.T, rawIDL string, encoding config.EncodingType) types.RemoteCodec { +func makeTestCodec(t *testing.T, rawIDL string) types.RemoteCodec { t.Helper() - var idl codec.IDL - if err := json.Unmarshal([]byte(rawIDL), &idl); err != nil { - t.Logf("failed to unmarshal test IDL: %s", err.Error()) - t.FailNow() - } - - testCodec, err := codec.NewIDLAccountCodec(idl, config.BuilderForEncoding(encoding)) + testCodec, err := codec.NewIDLAccountCodec(mustUnmarshalIDL(t, rawIDL), binary.LittleEndian()) if err != nil { t.Logf("failed to create new codec from test IDL: %s", err.Error()) t.FailNow() @@ -788,6 +753,23 @@ func fullStructIDL(t *testing.T) string { ) } +func fullTestIDL(t *testing.T) string { + t.Helper() + + // Combine all of the type definitions into one comma-separated string. + allTypes := strings.Join([]string{ + testStructIDL, + uint64BaseTypeIDL, + uint64SliceBaseTypeIDL, + }, ",") + + return fmt.Sprintf( + baseIDL, + allTypes, + strings.Join([]string{midLevelDynamicStructIDL, midLevelStaticStructIDL, innerDynamicStructIDL, innerStaticStructIDL, accountStructIDL}, ","), + ) +} + const ( baseIDL = `{ "version": "0.1.0", @@ -808,7 +790,7 @@ const ( {"name": "nestedStaticStruct","type": {"defined": "MidLevelStaticStruct"}}, {"name": "oracleID","type": "u8"}, {"name": "oracleIDs","type": {"array": ["u8",32]}}, - {"name": "accountstruct","type": {"defined": "accountstruct"}}, + {"name": "accountStruct","type": {"defined": "accountStruct"}}, {"name": "accounts","type": {"vec": "bytes"}} ] @@ -816,12 +798,12 @@ const ( }` accountStructIDL = `{ - "name": "accountstruct", + "name": "accountStruct", "type": { "kind": "struct", "fields": [ {"name": "account", "type": "bytes"}, - {"name": "accountstr", "type": {"array": ["u8",32]}} + {"name": "accountStr", "type": {"array": ["u8",32]}} ] } }` @@ -933,3 +915,13 @@ func (s *skipEventsChainReader) QueryKey(_ context.Context, _ types.BoundContrac s.t.Skip("QueryKey is not yet supported in Solana") return nil, nil } + +func mustUnmarshalIDL(t *testing.T, rawIDL string) codec.IDL { + var idl codec.IDL + if err := json.Unmarshal([]byte(rawIDL), &idl); err != nil { + t.Logf("failed to unmarshal test IDL: %s", err.Error()) + t.FailNow() + } + + return idl +} diff --git a/pkg/solana/chainreader/lookup.go b/pkg/solana/chainreader/lookup.go index b4295d20f..ccd7b44db 100644 --- a/pkg/solana/chainreader/lookup.go +++ b/pkg/solana/chainreader/lookup.go @@ -7,19 +7,19 @@ import ( ) type readValues struct { - address string - contract string - readName string + address string + contract string + genericName string } // lookup provides basic utilities for mapping a complete readIdentifier to // finite contract read information type lookup struct { mu sync.RWMutex - // contractReadNames maps a contract name to all available readNames (method, log, event, etc.) + // contractReadNames maps a contract name to all available namePairs (method, log, event, etc.) contractReadNames map[string][]string // readIdentifiers maps from a complete readIdentifier string to finite read data - // a readIdentifier is a combination of address, contract, and readName as a concatenated string + // a readIdentifier is a combination of address, contract, and chainSpecificName as a concatenated string readIdentifiers map[string]readValues } @@ -30,7 +30,7 @@ func newLookup() *lookup { } } -func (l *lookup) addReadNameForContract(contract, readName string) { +func (l *lookup) addReadNameForContract(contract string, genericName string) { l.mu.Lock() defer l.mu.Unlock() @@ -39,23 +39,23 @@ func (l *lookup) addReadNameForContract(contract, readName string) { readNames = []string{} } - l.contractReadNames[contract] = append(readNames, readName) + l.contractReadNames[contract] = append(readNames, genericName) } func (l *lookup) bindAddressForContract(contract, address string) { l.mu.Lock() defer l.mu.Unlock() - for _, readName := range l.contractReadNames[contract] { + for _, genericName := range l.contractReadNames[contract] { readIdentifier := types.BoundContract{ Address: address, Name: contract, - }.ReadIdentifier(readName) + }.ReadIdentifier(genericName) l.readIdentifiers[readIdentifier] = readValues{ - address: address, - contract: contract, - readName: readName, + address: address, + contract: contract, + genericName: genericName, } } } @@ -64,11 +64,11 @@ func (l *lookup) unbindAddressForContract(contract, address string) { l.mu.Lock() defer l.mu.Unlock() - for _, readName := range l.contractReadNames[contract] { + for _, genericName := range l.contractReadNames[contract] { readIdentifier := types.BoundContract{ Address: address, Name: contract, - }.ReadIdentifier(readName) + }.ReadIdentifier(genericName) delete(l.readIdentifiers, readIdentifier) } diff --git a/pkg/solana/codec/anchoridl.go b/pkg/solana/codec/anchoridl.go index e54710c5c..3fc296e97 100644 --- a/pkg/solana/codec/anchoridl.go +++ b/pkg/solana/codec/anchoridl.go @@ -312,6 +312,11 @@ type IdlTypeDefTyEnum struct { Variants IdlEnumVariantSlice `json:"variants,omitempty"` } +var NilIdlTypeDefTy = IdlTypeDef{Type: IdlTypeDefTy{ + Kind: "struct", + Fields: &IdlTypeDefStruct{}, +}} + type IdlTypeDefTy struct { Kind IdlTypeDefTyKind `json:"kind"` diff --git a/pkg/solana/codec/codec_entry.go b/pkg/solana/codec/codec_entry.go index bc42ae968..f22b05984 100644 --- a/pkg/solana/codec/codec_entry.go +++ b/pkg/solana/codec/codec_entry.go @@ -22,41 +22,51 @@ type Entry interface { type entry struct { // TODO this might not be needed in the end, it was handy to make tests simpler - offchainName string - onchainName string - reflectType reflect.Type - typeCodec commonencodings.TypeCodec - mod codec.Modifier + genericName string + chainSpecificName string + reflectType reflect.Type + typeCodec commonencodings.TypeCodec + mod codec.Modifier // includeDiscriminator during Encode adds a discriminator to the encoded bytes under an assumption that the provided value didn't have a discriminator. // During Decode includeDiscriminator removes discriminator from bytes under an assumption that the provided struct doesn't need a discriminator. includeDiscriminator bool discriminator Discriminator } -func NewAccountEntry(offchainName string, idlAccount IdlTypeDef, idlTypes IdlTypeDefSlice, includeDiscriminator bool, mod codec.Modifier, builder commonencodings.Builder) (Entry, error) { - _, accCodec, err := createCodecType(idlAccount, createRefs(idlTypes, builder), false) +type AccountIDLTypes struct { + Account IdlTypeDef + Types IdlTypeDefSlice +} + +func NewAccountEntry(offchainName string, idlTypes AccountIDLTypes, includeDiscriminator bool, mod codec.Modifier, builder commonencodings.Builder) (Entry, error) { + _, accCodec, err := createCodecType(idlTypes.Account, createRefs(idlTypes.Types, builder), false) if err != nil { return nil, err } return newEntry( offchainName, - idlAccount.Name, + idlTypes.Account.Name, accCodec, includeDiscriminator, mod, ), nil } -func NewInstructionArgsEntry(offChainName string, instructions IdlInstruction, idlTypes IdlTypeDefSlice, mod codec.Modifier, builder commonencodings.Builder) (Entry, error) { - _, instructionCodecArgs, err := asStruct(instructions.Args, createRefs(idlTypes, builder), instructions.Name, false, true) +type InstructionArgsIDLTypes struct { + Instruction IdlInstruction + Types IdlTypeDefSlice +} + +func NewInstructionArgsEntry(offChainName string, idlTypes InstructionArgsIDLTypes, mod codec.Modifier, builder commonencodings.Builder) (Entry, error) { + _, instructionCodecArgs, err := asStruct(idlTypes.Instruction.Args, createRefs(idlTypes.Types, builder), idlTypes.Instruction.Name, false, true) if err != nil { return nil, err } return newEntry( offChainName, - instructions.Name, + idlTypes.Instruction.Name, instructionCodecArgs, // Instruction arguments don't need a discriminator by default false, @@ -64,15 +74,20 @@ func NewInstructionArgsEntry(offChainName string, instructions IdlInstruction, i ), nil } -func NewEventArgsEntry(offChainName string, event IdlEvent, idlTypes IdlTypeDefSlice, includeDiscriminator bool, mod codec.Modifier, builder commonencodings.Builder) (Entry, error) { - _, eventCodec, err := asStruct(eventFieldsToFields(event.Fields), createRefs(idlTypes, builder), event.Name, false, false) +type EventIDLTypes struct { + Event IdlEvent + Types IdlTypeDefSlice +} + +func NewEventArgsEntry(offChainName string, idlTypes EventIDLTypes, includeDiscriminator bool, mod codec.Modifier, builder commonencodings.Builder) (Entry, error) { + _, eventCodec, err := asStruct(eventFieldsToFields(idlTypes.Event.Fields), createRefs(idlTypes.Types, builder), idlTypes.Event.Name, false, false) if err != nil { return nil, err } return newEntry( offChainName, - event.Name, + idlTypes.Event.Name, eventCodec, includeDiscriminator, mod, @@ -80,19 +95,19 @@ func NewEventArgsEntry(offChainName string, event IdlEvent, idlTypes IdlTypeDefS } func newEntry( - offchainName, onchainName string, + genericName, chainSpecificName string, typeCodec commonencodings.TypeCodec, includeDiscriminator bool, mod codec.Modifier, ) Entry { return &entry{ - offchainName: offchainName, - onchainName: onchainName, + genericName: genericName, + chainSpecificName: chainSpecificName, reflectType: typeCodec.GetType(), typeCodec: typeCodec, mod: ensureModifier(mod), includeDiscriminator: includeDiscriminator, - discriminator: *NewDiscriminator(onchainName), + discriminator: *NewDiscriminator(chainSpecificName), } } @@ -115,8 +130,8 @@ func (e *entry) Encode(value any, into []byte) ([]byte, error) { return []byte{}, nil } } - return nil, fmt.Errorf("%w: cannot encode nil value for offchainName: %q, onchainName: %q", - commontypes.ErrInvalidType, e.offchainName, e.onchainName) + return nil, fmt.Errorf("%w: cannot encode nil value for genericName: %q, chainSpecificName: %q", + commontypes.ErrInvalidType, e.genericName, e.chainSpecificName) } encodedVal, err := e.typeCodec.Encode(value, into) @@ -139,13 +154,13 @@ func (e *entry) Encode(value any, into []byte) ([]byte, error) { func (e *entry) Decode(encoded []byte) (any, []byte, error) { if e.includeDiscriminator { if len(encoded) < discriminatorLength { - return nil, nil, fmt.Errorf("%w: encoded data too short to contain discriminator for offchainName: %q, onchainName: %q", - commontypes.ErrInvalidType, e.offchainName, e.onchainName) + return nil, nil, fmt.Errorf("%w: encoded data too short to contain discriminator for genericName: %q, chainSpecificName: %q", + commontypes.ErrInvalidType, e.genericName, e.chainSpecificName) } if !bytes.Equal(e.discriminator.hashPrefix, encoded[:discriminatorLength]) { - return nil, nil, fmt.Errorf("%w: encoded data has a bad discriminator %v for offchainName: %q, onchainName: %q", - commontypes.ErrInvalidType, encoded[:discriminatorLength], e.offchainName, e.onchainName) + return nil, nil, fmt.Errorf("%w: encoded data has a bad discriminator %v for genericName: %q, chainSpecificName: %q", + commontypes.ErrInvalidType, encoded[:discriminatorLength], e.genericName, e.chainSpecificName) } encoded = encoded[discriminatorLength:] diff --git a/pkg/solana/codec/codec_test.go b/pkg/solana/codec/codec_test.go index 689d545ce..50b0366ac 100644 --- a/pkg/solana/codec/codec_test.go +++ b/pkg/solana/codec/codec_test.go @@ -122,7 +122,7 @@ func (it *codecInterfaceTester) GetCodec(t *testing.T) clcommontypes.Codec { codecEntryCfg := codecConfig.Configs[offChainName] codecEntryCfg.IDL = v.IDL codecEntryCfg.Type = v.ItemType - codecEntryCfg.OnChainName = v.IDLTypeName + codecEntryCfg.ChainSpecificName = v.IDLTypeName if offChainName != NilType { codecEntryCfg.ModifierConfigs = commoncodec.ModifiersConfig{ diff --git a/pkg/solana/codec/decoder.go b/pkg/solana/codec/decoder.go index 242dbc44f..ec734b36a 100644 --- a/pkg/solana/codec/decoder.go +++ b/pkg/solana/codec/decoder.go @@ -9,21 +9,27 @@ import ( ) type Decoder struct { - definitions map[string]Entry - codecFromTypeCodec encodings.CodecFromTypeCodec + definitions map[string]Entry + lenientFromTypeCodec encodings.LenientCodecFromTypeCodec } var _ commontypes.Decoder = &Decoder{} func (d *Decoder) Decode(ctx context.Context, raw []byte, into any, itemType string) (err error) { - if d.codecFromTypeCodec == nil { - d.codecFromTypeCodec = make(encodings.CodecFromTypeCodec) + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("recovered from: %v, while decoding %q", r, itemType) + } + }() + + if d.lenientFromTypeCodec == nil { + d.lenientFromTypeCodec = make(encodings.LenientCodecFromTypeCodec) for k, v := range d.definitions { - d.codecFromTypeCodec[k] = v + d.lenientFromTypeCodec[k] = v } } - return d.codecFromTypeCodec.Decode(ctx, raw, into, itemType) + return d.lenientFromTypeCodec.Decode(ctx, raw, into, itemType) } func (d *Decoder) GetMaxDecodingSize(_ context.Context, n int, itemType string) (int, error) { diff --git a/pkg/solana/codec/decoder_test.go b/pkg/solana/codec/decoder_test.go index ceea9644f..0f21c43e8 100644 --- a/pkg/solana/codec/decoder_test.go +++ b/pkg/solana/codec/decoder_test.go @@ -24,7 +24,7 @@ type testErrDecodeRemainingBytes struct { } func (t *testErrDecodeRemainingBytes) Decode(_ []byte) (interface{}, []byte, error) { - return nil, []byte{1}, nil + return struct{}{}, []byte{1}, nil } func TestDecoder_Decode_Errors(t *testing.T) { @@ -45,10 +45,10 @@ func TestDecoder_Decode_Errors(t *testing.T) { require.Error(t, d.Decode(tests.Context(t), []byte{}, &into, someType)) }) - t.Run("error when remaining bytes exist after decode", func(t *testing.T) { + t.Run("remaining bytes exist after decode is ok", func(t *testing.T) { d := &Decoder{definitions: map[string]Entry{}} d.definitions[someType] = &testErrDecodeRemainingBytes{} - require.Error(t, d.Decode(tests.Context(t), []byte{}, &into, someType)) + require.NoError(t, d.Decode(tests.Context(t), []byte{}, &into, someType)) }) } diff --git a/pkg/solana/codec/encoder.go b/pkg/solana/codec/encoder.go index 409fb0013..e3f734768 100644 --- a/pkg/solana/codec/encoder.go +++ b/pkg/solana/codec/encoder.go @@ -9,21 +9,27 @@ import ( ) type Encoder struct { - definitions map[string]Entry - codecFromTypeCodec encodings.CodecFromTypeCodec + definitions map[string]Entry + lenientCodecFromTypeCodec encodings.LenientCodecFromTypeCodec } var _ commontypes.Encoder = &Encoder{} func (e *Encoder) Encode(ctx context.Context, item any, itemType string) (res []byte, err error) { - if e.codecFromTypeCodec == nil { - e.codecFromTypeCodec = make(encodings.CodecFromTypeCodec) + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("recovered from: %v, while encoding %q", r, itemType) + } + }() + + if e.lenientCodecFromTypeCodec == nil { + e.lenientCodecFromTypeCodec = make(encodings.LenientCodecFromTypeCodec) for k, v := range e.definitions { - e.codecFromTypeCodec[k] = v + e.lenientCodecFromTypeCodec[k] = v } } - return e.codecFromTypeCodec.Encode(ctx, item, itemType) + return e.lenientCodecFromTypeCodec.Encode(ctx, item, itemType) } func (e *Encoder) GetMaxEncodingSize(_ context.Context, n int, itemType string) (int, error) { diff --git a/pkg/solana/codec/solana.go b/pkg/solana/codec/solana.go index 19fe40d3e..372e111aa 100644 --- a/pkg/solana/codec/solana.go +++ b/pkg/solana/codec/solana.go @@ -73,22 +73,14 @@ func NewCodec(conf Config) (commontypes.RemoteCodec, error) { return nil, err } - definition, err := findDefinitionFromIDL(cfg.Type, cfg.OnChainName, idl) + definition, err := FindDefinitionFromIDL(cfg.Type, cfg.ChainSpecificName, idl) if err != nil { return nil, err } - var cEntry Entry - switch v := definition.(type) { - case IdlTypeDef: - cEntry, err = NewAccountEntry(offChainName, v, idl.Types, true, mod, binary.LittleEndian()) - case IdlInstruction: - cEntry, err = NewInstructionArgsEntry(offChainName, v, idl.Types, mod, binary.LittleEndian()) - case IdlEvent: - cEntry, err = NewEventArgsEntry(offChainName, v, idl.Types, true, mod, binary.LittleEndian()) - } + cEntry, err := CreateCodecEntry(definition, offChainName, idl, mod) if err != nil { - return nil, fmt.Errorf("failed to create %q codec entry: %w", offChainName, err) + return nil, err } parsed.EncoderDefs[offChainName] = cEntry @@ -98,36 +90,62 @@ func NewCodec(conf Config) (commontypes.RemoteCodec, error) { return parsed.ToCodec() } -func findDefinitionFromIDL(cfgType ChainConfigType, onChainName string, idl IDL) (interface{}, error) { +func CreateCodecEntry(idlDefinition interface{}, offChainName string, idl IDL, mod commoncodec.Modifier) (entry Entry, err error) { + switch v := idlDefinition.(type) { + case IdlTypeDef: + entry, err = NewAccountEntry(offChainName, AccountIDLTypes{Account: v, Types: idl.Types}, true, mod, binary.LittleEndian()) + case IdlInstruction: + entry, err = NewInstructionArgsEntry(offChainName, InstructionArgsIDLTypes{Instruction: v, Types: idl.Types}, mod, binary.LittleEndian()) + case IdlEvent: + entry, err = NewEventArgsEntry(offChainName, EventIDLTypes{Event: v, Types: idl.Types}, true, mod, binary.LittleEndian()) + default: + return nil, fmt.Errorf("unknown codec IDL definition: %T", idlDefinition) + } + if err != nil { + return nil, fmt.Errorf("failed to create %q codec entry: %w", offChainName, err) + } + + return entry, nil +} + +func FindDefinitionFromIDL(cfgType ChainConfigType, chainSpecificName string, idl IDL) (interface{}, error) { // not the most efficient way to do this, but these slices should always be very, very small switch cfgType { case ChainConfigTypeAccountDef: for i := range idl.Accounts { - if idl.Accounts[i].Name == onChainName { + if idl.Accounts[i].Name == chainSpecificName { return idl.Accounts[i], nil } } - return nil, fmt.Errorf("failed to find account %q in IDL", onChainName) + return nil, fmt.Errorf("failed to find account %q in IDL", chainSpecificName) case ChainConfigTypeInstructionDef: for i := range idl.Instructions { - if idl.Instructions[i].Name == onChainName { + if idl.Instructions[i].Name == chainSpecificName { return idl.Instructions[i], nil } } - return nil, fmt.Errorf("failed to find instruction %q in IDL", onChainName) + return nil, fmt.Errorf("failed to find instruction %q in IDL", chainSpecificName) case ChainConfigTypeEventDef: for i := range idl.Events { - if idl.Events[i].Name == onChainName { + if idl.Events[i].Name == chainSpecificName { return idl.Events[i], nil } } - return nil, fmt.Errorf("failed to find event %q in IDL", onChainName) + return nil, fmt.Errorf("failed to find event %q in IDL", chainSpecificName) } return nil, fmt.Errorf("unknown type: %q", cfgType) } +func WrapItemType(forEncoding bool, contractName, itemType string, readType ChainConfigType) string { + if forEncoding { + return fmt.Sprintf("input.%s.%s", contractName, itemType) + } + + return fmt.Sprintf("output.%s.%s.%s", readType, contractName, itemType) +} + // NewIDLAccountCodec is for Anchor custom types func NewIDLAccountCodec(idl IDL, builder commonencodings.Builder) (commontypes.RemoteCodec, error) { return newIDLCoded(idl, builder, idl.Accounts, true) diff --git a/pkg/solana/codec/types.go b/pkg/solana/codec/types.go index e047b36ae..40852398e 100644 --- a/pkg/solana/codec/types.go +++ b/pkg/solana/codec/types.go @@ -16,8 +16,8 @@ type Config struct { } type ChainConfig struct { - IDL string `json:"typeIdl" toml:"typeIdl"` - OnChainName string `json:"onChainName" toml:"onChainName"` + IDL string `json:"typeIdl" toml:"typeIdl"` + ChainSpecificName string `json:"chainSpecificName" toml:"chainSpecificName"` // Type can be Solana Account, Instruction args, or TODO Event Type ChainConfigType `json:"type" toml:"type"` ModifierConfigs commoncodec.ModifiersConfig `json:"modifierConfigs,omitempty" toml:"modifierConfigs,omitempty"` diff --git a/pkg/solana/config/chain_reader.go b/pkg/solana/config/chain_reader.go index 4251624fe..57ccb9040 100644 --- a/pkg/solana/config/chain_reader.go +++ b/pkg/solana/config/chain_reader.go @@ -4,98 +4,79 @@ import ( "encoding/json" "fmt" - "github.com/gagliardetto/solana-go" - "github.com/gagliardetto/solana-go/rpc" - commoncodec "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" + commontypes "github.com/smartcontractkit/chainlink-common/pkg/types" + + "github.com/smartcontractkit/chainlink-solana/pkg/solana/codec" ) -type ChainReader struct { - Namespaces map[string]ChainReaderMethods `json:"namespaces" toml:"namespaces"` +type ContractReader struct { + Namespaces map[string]ChainContractReader `json:"namespaces"` } -type ChainReaderMethods struct { - Methods map[string]ChainDataReader `json:"methods" toml:"methods"` +type ChainContractReader struct { + codec.IDL `json:"anchorIDL"` + // Reads key is the off-chain name for this read. + Reads map[string]ReadDefinition `json:"reads"` + // TODO ContractPollingFilter same as EVM? } -type ChainDataReader struct { - 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"` - Procedure ChainReaderProcedure `json:"procedure" toml:"procedure"` +type ReadDefinition struct { + ChainSpecificName string `json:"chainSpecificName"` + ReadType ReadType `json:"readType,omitempty"` + InputModifications commoncodec.ModifiersConfig `json:"inputModifications,omitempty"` + OutputModifications commoncodec.ModifiersConfig `json:"outputModifications,omitempty"` } -type EncodingType int +type ReadType int const ( - EncodingTypeBorsh EncodingType = iota - EncodingTypeBincode - - encodingTypeBorshStr = "borsh" - encodingTypeBincodeStr = "bincode" + Account ReadType = iota + Event ) -func (t EncodingType) MarshalJSON() ([]byte, error) { - switch t { - case EncodingTypeBorsh: - return json.Marshal(encodingTypeBorshStr) - case EncodingTypeBincode: - return json.Marshal(encodingTypeBincodeStr) +func (r ReadType) String() string { + switch r { + case Account: + return "Account" + case Event: + return "Event" default: - return nil, fmt.Errorf("%w: unrecognized encoding type: %d", types.ErrInvalidConfig, t) + return fmt.Sprintf("Unknown(%d)", r) } } -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()) +func (c *ChainContractReader) UnmarshalJSON(bytes []byte) error { + rawJSON := make(map[string]json.RawMessage) + if err := json.Unmarshal(bytes, &rawJSON); err != nil { + return err } - switch str { - case encodingTypeBorshStr: - *t = EncodingTypeBorsh - case encodingTypeBincodeStr: - *t = EncodingTypeBincode - default: - return fmt.Errorf("%w: unrecognized encoding type: %s", types.ErrInvalidConfig, str) + idlBytes := rawJSON["anchorIDL"] + var rawString string + if err := json.Unmarshal(idlBytes, &rawString); err == nil { + if err = json.Unmarshal([]byte(rawString), &c.IDL); err != nil { + return fmt.Errorf("failed to parse anchorIDL string as IDL struct: %w", err) + } + return nil } - return nil -} - -type RPCOpts struct { - Encoding *solana.EncodingType `json:"encoding,omitempty"` - Commitment *rpc.CommitmentType `json:"commitment,omitempty"` - DataSlice *rpc.DataSlice `json:"dataSlice,omitempty"` -} + // If we didn't get a string, attempt to parse directly as an IDL object + if err := json.Unmarshal(idlBytes, &c.IDL); err != nil { + return fmt.Errorf("anchorIDL field is neither a valid JSON string nor a valid IDL object: %w", err) + } -type ChainReaderProcedure chainDataProcedureFields + if len(c.Accounts) == 0 && len(c.Events) == 0 { + return fmt.Errorf("namespace idl must have at least one account or event: %w", commontypes.ErrInvalidConfig) + } -type chainDataProcedureFields struct { - // IDLAccount refers to the account defined in the IDL. - IDLAccount string `json:"idlAccount,omitempty"` - // OutputModifications provides modifiers to convert chain data format to custom - // output formats. - OutputModifications commoncodec.ModifiersConfig `json:"outputModifications,omitempty"` - // RPCOpts provides optional configurations for commitment, encoding, and data - // slice offsets. - RPCOpts *RPCOpts `json:"rpcOpts,omitempty"` -} + if err := json.Unmarshal(rawJSON["reads"], &c.Reads); err != nil { + return err + } -// 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() + if len(c.Reads) == 0 { + return fmt.Errorf("namespace must have at least one read: %w", commontypes.ErrInvalidConfig) } + + return nil } diff --git a/pkg/solana/config/chain_reader_test.go b/pkg/solana/config/chain_reader_test.go index 7d290b50c..19bcedbe3 100644 --- a/pkg/solana/config/chain_reader_test.go +++ b/pkg/solana/config/chain_reader_test.go @@ -5,15 +5,14 @@ import ( "encoding/json" "testing" - "github.com/gagliardetto/solana-go" - "github.com/gagliardetto/solana-go/rpc" "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" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/codec/testutils" "github.com/smartcontractkit/chainlink-solana/pkg/solana/config" ) @@ -21,24 +20,33 @@ import ( //go:embed testChainReader_valid.json var validJSON string +//go:embed testChainReader_valid_with_IDL_as_string.json +var validJSONWithIDLAsString 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.Run("valid unmarshal with idl as struct", func(t *testing.T) { t.Parallel() - var result config.ChainReader + var result config.ContractReader require.NoError(t, json.Unmarshal([]byte(validJSON), &result)) assert.Equal(t, validChainReaderConfig, result) }) + t.Run("valid unmarshal with idl as string", func(t *testing.T) { + var result config.ContractReader + require.NoError(t, json.Unmarshal([]byte(validJSONWithIDLAsString), &result)) + assert.Equal(t, validChainReaderConfig, result) + }) + t.Run("invalid unmarshal", func(t *testing.T) { t.Parallel() - var result config.ChainReader + var result config.ContractReader require.ErrorIs(t, json.Unmarshal([]byte(invalidJSON), &result), types.ErrInvalidConfig) }) @@ -49,79 +57,42 @@ func TestChainReaderConfig(t *testing.T) { require.NoError(t, err) - var conf config.ChainReader + var conf config.ContractReader 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 nilIDL = codec.IDL{ + Version: "0.1.0", + Name: "myProgram", + Accounts: codec.IdlTypeDefSlice{ + {Name: "NilType", Type: codec.IdlTypeDefTy{Kind: codec.IdlTypeDefTyKindStruct, Fields: &codec.IdlTypeDefStruct{}}}, + }, } -var ( - encodingBase64 = solana.EncodingBase64 - commitment = rpc.CommitmentFinalized - offset = uint64(10) - length = uint64(10) -) - -var validChainReaderConfig = config.ChainReader{ - Namespaces: map[string]config.ChainReaderMethods{ +var validChainReaderConfig = config.ContractReader{ + Namespaces: map[string]config.ChainContractReader{ "Contract": { - Methods: map[string]config.ChainDataReader{ + IDL: nilIDL, + Reads: map[string]config.ReadDefinition{ "Method": { - AnchorIDL: "test idl 1", - Encoding: config.EncodingTypeBorsh, - Procedure: config.ChainReaderProcedure{ - IDLAccount: testutils.TestStructWithNestedStruct, - }, + ChainSpecificName: testutils.TestStructWithNestedStruct, }, "MethodWithOpts": { - AnchorIDL: "test idl 2", - Encoding: config.EncodingTypeBorsh, - Procedure: config.ChainReaderProcedure{ - IDLAccount: testutils.TestStructWithNestedStruct, - OutputModifications: codeccommon.ModifiersConfig{ - &codeccommon.PropertyExtractorConfig{FieldName: "DurationVal"}, - }, - RPCOpts: &config.RPCOpts{ - Encoding: &encodingBase64, - Commitment: &commitment, - DataSlice: &rpc.DataSlice{ - Offset: &offset, - Length: &length, - }, - }, + ChainSpecificName: testutils.TestStructWithNestedStruct, + OutputModifications: codeccommon.ModifiersConfig{ + &codeccommon.PropertyExtractorConfig{FieldName: "DurationVal"}, }, }, }, }, "OtherContract": { - Methods: map[string]config.ChainDataReader{ + IDL: nilIDL, + Reads: map[string]config.ReadDefinition{ "Method": { - AnchorIDL: "test idl 3", - Encoding: config.EncodingTypeBincode, - Procedure: config.ChainReaderProcedure{ - IDLAccount: testutils.TestStructWithNestedStruct, - }, + ChainSpecificName: testutils.TestStructWithNestedStruct, }, }, }, diff --git a/pkg/solana/config/testChainReader_invalid.json b/pkg/solana/config/testChainReader_invalid.json index 98caa8fcc..925f8eba0 100644 --- a/pkg/solana/config/testChainReader_invalid.json +++ b/pkg/solana/config/testChainReader_invalid.json @@ -1,13 +1,14 @@ { "namespaces": { "Contract": { - "methods": { - "Method": { - "anchorIDL": "test idl 1", - "encoding": "invalid", - "procedure": { - "idlAccount": "StructWithNestedStruct" - } + "anchorIDL": { + "version": "0.1.0", + "name": "myProgram" + }, + "OtherContract": { + "anchorIDL": { + "version": "0.1.0", + "name": "myProgram" } } } diff --git a/pkg/solana/config/testChainReader_valid.json b/pkg/solana/config/testChainReader_valid.json index ca75a936b..a813988c0 100644 --- a/pkg/solana/config/testChainReader_valid.json +++ b/pkg/solana/config/testChainReader_valid.json @@ -1,43 +1,54 @@ { - "namespaces": { - "Contract": { - "methods": { - "Method": { - "anchorIDL": "test idl 1", - "encoding": "borsh", - "procedure": { - "idlAccount": "StructWithNestedStruct" + "namespaces":{ + "Contract":{ + "anchorIDL":{ + "version":"0.1.0", + "name":"myProgram", + "accounts":[ + { + "name":"NilType", + "type":{ + "kind":"struct", + "fields":[ + + ] + } } + ] + }, + "reads":{ + "Method":{ + "chainSpecificName":"StructWithNestedStruct" }, - "MethodWithOpts": { - "anchorIDL": "test idl 2", - "encoding": "borsh", - "procedure": { - "idlAccount": "StructWithNestedStruct", - "outputModifications": [{ - "Type": "extract property", - "FieldName": "DurationVal" - }], - "rpcOpts": { - "encoding": "base64", - "commitment": "finalized", - "dataSlice": { - "offset": 10, - "length": 10 - } + "MethodWithOpts":{ + "chainSpecificName":"StructWithNestedStruct", + "outputModifications":[ + { + "FieldName":"DurationVal", + "Type":"extract property" } - } + ] } } }, - "OtherContract": { - "methods": { - "Method": { - "anchorIDL": "test idl 3", - "encoding": "bincode", - "procedure": { - "idlAccount": "StructWithNestedStruct" + "OtherContract":{ + "anchorIDL":{ + "version":"0.1.0", + "name":"myProgram", + "accounts":[ + { + "name":"NilType", + "type":{ + "kind":"struct", + "fields":[ + ] + } } + ] + }, + "reads":{ + "Method":{ + "chainSpecificName":"StructWithNestedStruct" } } } diff --git a/pkg/solana/config/testChainReader_valid_with_IDL_as_string.json b/pkg/solana/config/testChainReader_valid_with_IDL_as_string.json new file mode 100644 index 000000000..700c7b768 --- /dev/null +++ b/pkg/solana/config/testChainReader_valid_with_IDL_as_string.json @@ -0,0 +1,57 @@ +{ + "namespaces":{ + "Contract":{ + "anchorIDL":{ + "version":"0.1.0", + "name":"myProgram", + "accounts":[ + { + "name":"NilType", + "type":{ + "kind":"struct", + "fields":[ + + ] + } + } + ] + }, + "reads":{ + "Method":{ + "chainSpecificName":"StructWithNestedStruct" + }, + "MethodWithOpts":{ + "chainSpecificName":"StructWithNestedStruct", + "outputModifications":[ + { + "FieldName":"DurationVal", + "Type":"extract property" + } + ] + } + } + }, + "OtherContract":{ + "anchorIDL":{ + "version":"0.1.0", + "name":"myProgram", + "accounts":[ + { + "name":"NilType", + "type":{ + "kind":"struct", + "fields":[ + + ] + } + } + ] + }, + "reads":{ + "Method":{ + "chainSpecificName":"StructWithNestedStruct" + } + } + } + } +} \ No newline at end of file