diff --git a/pkg/solana/chainreader/chain_reader.go b/pkg/solana/chainreader/chain_reader.go index d017eb25d..b7fda7346 100644 --- a/pkg/solana/chainreader/chain_reader.go +++ b/pkg/solana/chainreader/chain_reader.go @@ -10,7 +10,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 +33,8 @@ type SolanaChainReaderService struct { // internal values bindings namespaceBindings lookup *lookup + parsed *codec.ParsedTypes + codec types.RemoteCodec // service state management wg sync.WaitGroup @@ -45,19 +47,25 @@ 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 } - return svc, nil + if svcCodec, err := svc.parsed.ToCodec(); err != nil { + return nil, err + } else { + svc.codec = svcCodec + return svc, nil + } } // Name implements the services.ServiceCtx interface and returns the logger service name. @@ -220,51 +228,106 @@ func (s *SolanaChainReaderService) CreateContractType(readIdentifier string, for return s.bindings.CreateType(values.contract, values.readName, forEncoding) } -func (s *SolanaChainReaderService) init(namespaces map[string]config.ChainReaderMethods) error { - for namespace, methods := range namespaces { - for methodName, method := range methods.Methods { +func (s *SolanaChainReaderService) addCodecDef(isInput bool, namespace, itemType string, readType codec.ChainConfigType, idl codec.IDL, idlDefinition interface{}, modCfg commoncodec.ModifiersConfig) error { + mod, err := modCfg.ToModifier(codec.DecoderHooks...) + if err != nil { + return err + } + + cEntry, err := codec.CreateCodecEntry(idlDefinition, itemType, idl, mod) + if err != nil { + return err + } + + s.parsed.EncoderDefs[codec.WrapItemType(isInput, namespace, itemType, readType)] = cEntry + return nil +} + +func (s *SolanaChainReaderService) init(namespaces map[string]config.ChainContractReader) error { + for namespace, namespaceReads := range namespaces { + for readName, read := range namespaceReads.Reads { var idl codec.IDL - if err := json.Unmarshal([]byte(method.AnchorIDL), &idl); err != nil { + if err := json.Unmarshal([]byte(namespaceReads.IDL), &idl); err != nil { return err } - idlCodec, err := codec.NewIDLAccountCodec(idl, config.BuilderForEncoding(method.Encoding)) + injectAddressModifier(read.InputModifications, read.OutputModifications) + idlDef, err := codec.FindDefinitionFromIDL(codec.ChainConfigTypeAccountDef, read.ChainSpecificName, idl) if err != nil { return err } - s.lookup.addReadNameForContract(namespace, methodName) - - procedure := method.Procedure + 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 onchain name: %q, of type: %q", accountIDLDef, readName, read.ChainSpecificName, read.ReadType) + } + if err = s.addAccountRead(namespace, readName, idl, accountIDLTypes{ + Account: accountIDLDef, + Types: idl.Types, + }, read); err != nil { + return err + } + case config.Log: + eventIDlDef, isOk := idlDef.(codec.IdlEvent) + if !isOk { + return fmt.Errorf("unexpected type %T from IDL definition for log read: %q, with onchain name: %q, of type: %q", eventIDlDef, readName, read.ChainSpecificName, read.ReadType) + } + // TODO s.addLogRead() + default: + return fmt.Errorf("unexpected read type %q for: %q in namespace: %q", read.ReadType, readName, namespace) + } + } + } - injectAddressModifier(procedure.OutputModifications) + return nil +} - mod, err := procedure.OutputModifications.ToModifier(codec.DecoderHooks...) - if err != nil { - return err - } +type accountIDLTypes struct { + Account codec.IdlTypeDef + Types codec.IdlTypeDefSlice +} - codecWithModifiers, err := codec.NewNamedModifierCodec(idlCodec, procedure.IDLAccount, mod) - if err != nil { - return err - } +func (s *SolanaChainReaderService) addAccountRead(namespace string, itemType string, idl codec.IDL, idlType accountIDLTypes, readDefinition config.ReadDefinition) error { + // TODO if readDefinition.HasPDASeedInput{ + // if err := s.addCodecDef(false, namespace, itemType, codec.ChainConfigTypeAccountDef, idl,idlType, readDefinition.InputModifications,; err != nil { + // return err + // } + // } - s.bindings.AddReadBinding(namespace, methodName, newAccountReadBinding( - procedure.IDLAccount, - codecWithModifiers, - createRPCOpts(procedure.RPCOpts), - )) - } + if err := s.addCodecDef(false, namespace, itemType, codec.ChainConfigTypeAccountDef, idl, idlType, readDefinition.OutputModifications); err != nil { + return err } + s.lookup.addReadNameForContract(namespace, itemType) + s.bindings.AddReadBinding(namespace, itemType, newAccountReadBinding( + readDefinition.ChainSpecificName, + // TODO codec is not created at this point, set codec for all bindings after init + s.codec, + createRPCOpts(readDefinition.RPCOpts), + )) + + return nil +} + +func (s *SolanaChainReaderService) addLogRead() error { + // TODO: init codec types and modifiers, do address lookup here if needed? ... 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) { +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 + } + } + for i, modConfig := range outputModifications { - if addrModifierConfig, ok := modConfig.(*codeccommon.AddressBytesToStringModifierConfig); ok { + if addrModifierConfig, ok := modConfig.(*commoncodec.AddressBytesToStringModifierConfig); ok { addrModifierConfig.Modifier = codec.SolanaAddressModifier{} outputModifications[i] = addrModifierConfig } diff --git a/pkg/solana/chainreader/chain_reader_test.go b/pkg/solana/chainreader/chain_reader_test.go index 4652f5199..ca3f0f3e5 100644 --- a/pkg/solana/chainreader/chain_reader_test.go +++ b/pkg/solana/chainreader/chain_reader_test.go @@ -64,7 +64,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 +289,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: 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 +383,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 } @@ -420,70 +418,55 @@ func (r *chainReaderInterfaceTester) Setup(t *testing.T) { 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: 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", + RPCOpts: &config.RPCOpts{ + Encoding: &encodingBase64, + Commitment: &commitment, + DataSlice: &rpc.DataSlice{ + Offset: &offset, + Length: &length, }, }, }, 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", + 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", + OutputModifications: codeccommon.ModifiersConfig{ + &codeccommon.AddressBytesToStringModifierConfig{ + Fields: []string{"Accountstruct.Accountstr"}, }, + &codeccommon.HardCodeModifierConfig{OffChainValues: map[string]any{"ExtraField": AnyExtraValue}}, }, }, }, }, AnySecondContractName: { - Methods: map[string]config.ChainDataReader{ + IDL: 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", + OutputModifications: codeccommon.ModifiersConfig{ + &codeccommon.PropertyExtractorConfig{FieldName: "I"}, }, }, }, @@ -574,7 +557,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 +570,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 +582,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 +607,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,7 +743,7 @@ 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 @@ -769,7 +752,7 @@ func makeTestCodec(t *testing.T, rawIDL string, encoding config.EncodingType) ty t.FailNow() } - testCodec, err := codec.NewIDLAccountCodec(idl, config.BuilderForEncoding(encoding)) + testCodec, err := codec.NewIDLAccountCodec(idl, binary.LittleEndian()) if err != nil { t.Logf("failed to create new codec from test IDL: %s", err.Error()) t.FailNow() @@ -788,6 +771,27 @@ 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{ + midLevelDynamicStructIDL, + midLevelStaticStructIDL, + innerDynamicStructIDL, + innerStaticStructIDL, + accountStructIDL, + uint64BaseTypeIDL, + uint64SliceBaseTypeIDL, + }, ",") + + return fmt.Sprintf( + baseIDL, + testStructIDL, + allTypes, + ) +} + const ( baseIDL = `{ "version": "0.1.0", diff --git a/pkg/solana/codec/codec_entry.go b/pkg/solana/codec/codec_entry.go index bc42ae968..6342055b4 100644 --- a/pkg/solana/codec/codec_entry.go +++ b/pkg/solana/codec/codec_entry.go @@ -33,30 +33,40 @@ type entry struct { 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, diff --git a/pkg/solana/codec/solana.go b/pkg/solana/codec/solana.go index 19fe40d3e..3ecada671 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.OnChainName, 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,7 +90,25 @@ 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, onChainName 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: @@ -128,6 +138,14 @@ func findDefinitionFromIDL(cfgType ChainConfigType, onChainName string, idl IDL) return nil, fmt.Errorf("unknown type: %q", cfgType) } +func WrapItemType(isInput bool, contractName, itemType string, readType ChainConfigType) string { + if isInput { + 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/config/chain_reader.go b/pkg/solana/config/chain_reader.go index 4251624fe..53ae30fe3 100644 --- a/pkg/solana/config/chain_reader.go +++ b/pkg/solana/config/chain_reader.go @@ -1,101 +1,58 @@ package config 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" ) -type ChainReader struct { - Namespaces map[string]ChainReaderMethods `json:"namespaces" toml:"namespaces"` +type ContractReader struct { + Namespaces map[string]ChainContractReader `json:"namespaces" toml:"namespaces"` } -type ChainReaderMethods struct { - Methods map[string]ChainDataReader `json:"methods" toml:"methods"` +type ChainContractReader struct { + IDL string `json:"anchorIDL" toml:"anchorIDL"` + // Reads key is the off-chain name for this read. + Reads map[string]ReadDefinition + // 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"` + RPCOpts *RPCOpts `json:"rpcOpts,omitempty"` + + // TODO EventDefinitions *EventDefinitions similar to EVM? + // TODO Lookup details for PDAs and lookup tables to be merged with CW + //LookupTables *LookupTables + //Accounts *[]Lookup } -type EncodingType int +type ReadType int const ( - EncodingTypeBorsh EncodingType = iota - EncodingTypeBincode - - encodingTypeBorshStr = "borsh" - encodingTypeBincodeStr = "bincode" + Account ReadType = iota + Log ) -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 Log: + return "Log" 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()) - } - - 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 RPCOpts struct { Encoding *solana.EncodingType `json:"encoding,omitempty"` Commitment *rpc.CommitmentType `json:"commitment,omitempty"` DataSlice *rpc.DataSlice `json:"dataSlice,omitempty"` } - -type ChainReaderProcedure chainDataProcedureFields - -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"` -} - -// 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 index 7d290b50c..876a0ca6a 100644 --- a/pkg/solana/config/chain_reader_test.go +++ b/pkg/solana/config/chain_reader_test.go @@ -11,7 +11,6 @@ import ( "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" @@ -30,7 +29,7 @@ func TestChainReaderConfig(t *testing.T) { t.Run("valid unmarshal", 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) }) @@ -38,7 +37,7 @@ func TestChainReaderConfig(t *testing.T) { 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,33 +48,13 @@ 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 ( encodingBase64 = solana.EncodingBase64 commitment = rpc.CommitmentFinalized @@ -83,45 +62,33 @@ var ( 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{ + 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"}, + }, + RPCOpts: &config.RPCOpts{ + Encoding: &encodingBase64, + Commitment: &commitment, + DataSlice: &rpc.DataSlice{ + Offset: &offset, + Length: &length, }, }, }, }, }, "OtherContract": { - Methods: map[string]config.ChainDataReader{ + 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..c7313f8d5 100644 --- a/pkg/solana/config/testChainReader_invalid.json +++ b/pkg/solana/config/testChainReader_invalid.json @@ -1,15 +1,38 @@ { "namespaces": { - "Contract": { - "methods": { - "Method": { - "anchorIDL": "test idl 1", - "encoding": "invalid", - "procedure": { - "idlAccount": "StructWithNestedStruct" + "example_namespace": { + "anchorIDL": { + "name": "example_invalid_idl_name", + "version": "1.0.0" + }, + "reads": { + "someRead": { + "chainSpecificName": "some_chain_specific_name", + "readType": 0, + "inputModifications": [ + { + "path": "someInputPath", + "action": "replace", + "value": "newValue" + } + ], + "outputModifications": [ + { + "path": "someOutputPath", + "action": "extract", + "value": "desiredField" + } + ], + "rpcOpts": { + "encoding": "base64", + "commitment": "processed", + "dataSlice": { + "offset": 0, + "length": 32 + } } } } } } -} \ No newline at end of file +} diff --git a/pkg/solana/config/testChainReader_valid.json b/pkg/solana/config/testChainReader_valid.json index ca75a936b..a690ba7f5 100644 --- a/pkg/solana/config/testChainReader_valid.json +++ b/pkg/solana/config/testChainReader_valid.json @@ -1,45 +1,40 @@ { "namespaces": { "Contract": { - "methods": { + "anchorIDL": "", + "reads": { "Method": { - "anchorIDL": "test idl 1", - "encoding": "borsh", - "procedure": { - "idlAccount": "StructWithNestedStruct" - } + "chainSpecificName": "StructWithNestedStruct", + "readType": 0 }, "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 - } + "chainSpecificName": "StructWithNestedStruct", + "readType": 0, + "rpcOpts": { + "encoding": "base64", + "commitment": "finalized", + "dataSlice": { + "offset": 10, + "length": 10 } - } + }, + "outputModifications": [ + { + "fieldName": "DurationVal", + "type": "extract property" + } + ] } } }, "OtherContract": { - "methods": { + "anchorIDL": "", + "reads": { "Method": { - "anchorIDL": "test idl 3", - "encoding": "bincode", - "procedure": { - "idlAccount": "StructWithNestedStruct" - } + "chainSpecificName": "StructWithNestedStruct", + "readType": 0 } } } } -} \ No newline at end of file +}