From 6ee164165f45db44c33edc0943db6f2ef8d37cfb Mon Sep 17 00:00:00 2001 From: ilija Date: Wed, 29 Jan 2025 16:57:03 +0100 Subject: [PATCH 1/8] Implement CR namespace group addr sharing and fix naming inconsistencies --- pkg/solana/chainreader/batch.go | 34 +++---- pkg/solana/chainreader/bindings.go | 120 ++++++++++++++++++------ pkg/solana/chainreader/bindings_test.go | 10 +- pkg/solana/chainreader/chain_reader.go | 63 +++++++------ pkg/solana/config/chain_reader.go | 6 ++ 5 files changed, 152 insertions(+), 81 deletions(-) diff --git a/pkg/solana/chainreader/batch.go b/pkg/solana/chainreader/batch.go index 91995bb80..af50f3a38 100644 --- a/pkg/solana/chainreader/batch.go +++ b/pkg/solana/chainreader/batch.go @@ -11,15 +11,15 @@ import ( ) type call struct { - ContractName, ReadName string - Params, ReturnVal any + Namespace, ReadName string + Params, ReturnVal any } type batchResultWithErr struct { - address string - contractName, readName string - returnVal any - err error + address string + namespace, readName string + returnVal any + err error } var ( @@ -30,16 +30,16 @@ type MultipleAccountGetter interface { GetMultipleAccountData(context.Context, ...solana.PublicKey) ([][]byte, error) } -func doMethodBatchCall(ctx context.Context, client MultipleAccountGetter, bindings namespaceBindings, batch []call) ([]batchResultWithErr, error) { +func doMethodBatchCall(ctx context.Context, client MultipleAccountGetter, bindingsRegistry bindingsRegistry, batch []call) ([]batchResultWithErr, error) { // Create the list of public keys to fetch keys := make([]solana.PublicKey, len(batch)) for idx, call := range batch { - binding, err := bindings.GetReadBinding(call.ContractName, call.ReadName) + rBinding, err := bindingsRegistry.GetReadBinding(call.Namespace, call.ReadName) if err != nil { return nil, err } - key, err := binding.GetAddress(ctx, call.Params) + key, err := rBinding.GetAddress(ctx, call.Params) if err != nil { return nil, fmt.Errorf("failed to get address for %s account read: %w", call.ReadName, err) } @@ -57,10 +57,10 @@ func doMethodBatchCall(ctx context.Context, client MultipleAccountGetter, bindin // decode batch call results for idx, call := range batch { results[idx] = batchResultWithErr{ - address: keys[idx].String(), - contractName: call.ContractName, - readName: call.ReadName, - returnVal: call.ReturnVal, + address: keys[idx].String(), + namespace: call.Namespace, + readName: call.ReadName, + returnVal: call.ReturnVal, } if data[idx] == nil || len(data[idx]) == 0 { @@ -69,7 +69,7 @@ func doMethodBatchCall(ctx context.Context, client MultipleAccountGetter, bindin continue } - binding, err := bindings.GetReadBinding(results[idx].contractName, results[idx].readName) + rBinding, err := bindingsRegistry.GetReadBinding(results[idx].namespace, results[idx].readName) if err != nil { results[idx].err = err @@ -80,12 +80,12 @@ func doMethodBatchCall(ctx context.Context, client MultipleAccountGetter, bindin if !isValue { results[idx].err = errors.Join( results[idx].err, - binding.Decode(ctx, data[idx], results[idx].returnVal), + rBinding.Decode(ctx, data[idx], results[idx].returnVal), ) continue } - contractType, err := binding.CreateType(false) + contractType, err := rBinding.CreateType(false) if err != nil { results[idx].err = err @@ -94,7 +94,7 @@ func doMethodBatchCall(ctx context.Context, client MultipleAccountGetter, bindin results[idx].err = errors.Join( results[idx].err, - binding.Decode(ctx, data[idx], contractType), + rBinding.Decode(ctx, data[idx], contractType), ) value, err := values.Wrap(contractType) diff --git a/pkg/solana/chainreader/bindings.go b/pkg/solana/chainreader/bindings.go index eba7eca15..1b24e868c 100644 --- a/pkg/solana/chainreader/bindings.go +++ b/pkg/solana/chainreader/bindings.go @@ -3,6 +3,7 @@ package chainreader import ( "context" "fmt" + "sync" "github.com/gagliardetto/solana-go" @@ -21,73 +22,134 @@ type readBinding interface { QueryKey(context.Context, query.KeyFilter, query.LimitAndSort, any) ([]types.Sequence, error) } -// key is namespace -type namespaceBindings map[string]readNameBindings +type bindingsRegistry struct { + // key is namespace + namespaceBindings map[string]readNameBindings + // key is namespace + addressShareGroups map[string]*addressShareGroup +} -// key is method name +// key is read name type readNameBindings map[string]readBinding -func (b namespaceBindings) AddReadBinding(namespace, readName string, reader readBinding) { - if _, nbsExists := b[namespace]; !nbsExists { - b[namespace] = readNameBindings{} +type addressShareGroup struct { + address solana.PublicKey + mux sync.Mutex + group []string +} + +func (b *bindingsRegistry) AddReadBinding(namespace, readName string, rBinding readBinding) { + if _, nbsExists := b.namespaceBindings[namespace]; !nbsExists { + b.namespaceBindings[namespace] = readNameBindings{} } - b[namespace][readName] = reader + b.namespaceBindings[namespace][readName] = rBinding } -func (b namespaceBindings) GetReadBinding(namespace, readName string) (readBinding, error) { - nbs, nbsExists := b[namespace] - if !nbsExists { - return nil, fmt.Errorf("%w: no read binding exists for %s", types.ErrInvalidConfig, namespace) +func (b *bindingsRegistry) GetReadBinding(namespace, readName string) (readBinding, error) { + rBindings, nameSpaceExists := b.namespaceBindings[namespace] + if !nameSpaceExists { + return nil, fmt.Errorf("%w: no read binding exists for namespace: %q", types.ErrInvalidConfig, namespace) } - rbs, rbsExists := nbs[readName] - if !rbsExists { - return nil, fmt.Errorf("%w: no read binding exists for %s and %s", types.ErrInvalidConfig, namespace, readName) + rBinding, rBindingExists := rBindings[readName] + if !rBindingExists { + return nil, fmt.Errorf("%w: no read binding exists for namespace: %q read: %q", types.ErrInvalidConfig, namespace, readName) } - return rbs, nil + return rBinding, nil } -func (b namespaceBindings) CreateType(namespace, readName string, forEncoding bool) (any, error) { - binding, err := b.GetReadBinding(namespace, readName) +func (b *bindingsRegistry) CreateType(namespace, readName string, forEncoding bool) (any, error) { + rBinding, err := b.GetReadBinding(namespace, readName) if err != nil { return nil, err } - return binding.CreateType(forEncoding) + return rBinding.CreateType(forEncoding) } -func (b namespaceBindings) Bind(binding types.BoundContract) error { - bnd, nbsExist := b[binding.Name] - if !nbsExist { - return fmt.Errorf("%w: no namespace named %s", types.ErrInvalidConfig, binding.Name) +func (b *bindingsRegistry) Bind(boundContract *types.BoundContract) error { + if err := b.handleAddressSharing(boundContract); err != nil { + return err } - key, err := solana.PublicKeyFromBase58(binding.Address) + rBindings, nameSpaceExists := b.namespaceBindings[boundContract.Name] + if !nameSpaceExists { + return fmt.Errorf("%w: no namespace named: %q", types.ErrInvalidConfig, boundContract.Name) + } + + key, err := solana.PublicKeyFromBase58(boundContract.Address) if err != nil { return err } - for _, rb := range bnd { - rb.SetAddress(key) + for _, rBinding := range rBindings { + rBinding.SetAddress(key) } return nil } -func (b namespaceBindings) SetCodecs(codec types.RemoteCodec) { - for _, nbs := range b { +func (b *bindingsRegistry) SetCodecs(codec types.RemoteCodec) { + for _, nbs := range b.namespaceBindings { for _, rb := range nbs { rb.SetCodec(codec) } } } -func (b namespaceBindings) SetModifiers(modifier commoncodec.Modifier) { - for _, nbs := range b { +func (b *bindingsRegistry) SetModifiers(modifier commoncodec.Modifier) { + for _, nbs := range b.namespaceBindings { for _, rb := range nbs { rb.SetModifier(modifier) } } } + + +func (b *bindingsRegistry) handleAddressSharing(boundContract *types.BoundContract) error { + shareGroup, sharesAddress := b.addressShareGroups[boundContract.Name] + if !sharesAddress { + return nil + } + + shareGroup.mux.Lock() + defer shareGroup.mux.Unlock() + + // set shared address to the binding address + shareGroupAddress := shareGroup.address + if shareGroupAddress.IsZero() { + key, err := solana.PublicKeyFromBase58(boundContract.Address) + if err != nil { + return err + } + shareGroup.address = key + } + + if boundContract.Address != shareGroupAddress.String() && boundContract.Address != "" { + return fmt.Errorf("binding: %q shares address: %q with namespaceBindings: %v and cannot be bound with a different address", boundContract.Name, shareGroup.address, shareGroup.group) + } + + boundContract.Address = shareGroupAddress.String() + return nil +} + +func (b *bindingsRegistry) initAddressSharing(addressShareGroups [][]string) error { + b.addressShareGroups = make(map[string]*addressShareGroup) + for _, group := range addressShareGroups { + shareGroup := &addressShareGroup{ + address: solana.PublicKey{}, + group: group, + } + + for _, namespace := range group { + if _, alreadySharesAddress := b.addressShareGroups[namespace]; alreadySharesAddress { + return fmt.Errorf("namespace %q can't share address with two different groups", namespace) + } + b.addressShareGroups[namespace] = shareGroup + } + } + + return nil +} diff --git a/pkg/solana/chainreader/bindings_test.go b/pkg/solana/chainreader/bindings_test.go index 51c583c52..70e2d9c8c 100644 --- a/pkg/solana/chainreader/bindings_test.go +++ b/pkg/solana/chainreader/bindings_test.go @@ -21,13 +21,13 @@ func TestBindings_CreateType(t *testing.T) { t.Parallel() expected := 8 + bdRegistry := bindingsRegistry{namespaceBindings: make(map[string]readNameBindings)} binding := new(mockBinding) - bindings := namespaceBindings{} - bindings.AddReadBinding("A", "B", binding) + bdRegistry.AddReadBinding("A", "B", binding) binding.On("CreateType", mock.Anything).Return(expected, nil) - returned, err := bindings.CreateType("A", "B", true) + returned, err := bdRegistry.CreateType("A", "B", true) require.NoError(t, err) assert.Equal(t, expected, returned) @@ -36,9 +36,9 @@ func TestBindings_CreateType(t *testing.T) { t.Run("returns error when binding does not exist", func(t *testing.T) { t.Parallel() - bindings := namespaceBindings{} + bdRegistry := bindingsRegistry{namespaceBindings: make(map[string]readNameBindings)} - _, err := bindings.CreateType("A", "B", true) + _, err := bdRegistry.CreateType("A", "B", true) require.ErrorIs(t, err, types.ErrInvalidConfig) }) diff --git a/pkg/solana/chainreader/chain_reader.go b/pkg/solana/chainreader/chain_reader.go index f801b62f2..682827d62 100644 --- a/pkg/solana/chainreader/chain_reader.go +++ b/pkg/solana/chainreader/chain_reader.go @@ -40,10 +40,10 @@ type ContractReaderService struct { reader EventsReader // internal values - bindings namespaceBindings - lookup *lookup - parsed *codec.ParsedTypes - codec types.RemoteCodec + bdRegistry bindingsRegistry + lookup *lookup + parsed *codec.ParsedTypes + codec types.RemoteCodec filters []logpoller.Filter // service state management @@ -64,16 +64,20 @@ func NewContractReaderService( reader EventsReader, ) (*ContractReaderService, error) { svc := &ContractReaderService{ - lggr: logger.Named(lggr, ServiceName), - client: dataReader, - bindings: namespaceBindings{}, - lookup: newLookup(), - parsed: &codec.ParsedTypes{EncoderDefs: map[string]codec.Entry{}, DecoderDefs: map[string]codec.Entry{}}, + lggr: logger.Named(lggr, ServiceName), + client: dataReader, + bdRegistry: bindingsRegistry{}, + lookup: newLookup(), + parsed: &codec.ParsedTypes{EncoderDefs: map[string]codec.Entry{}, DecoderDefs: map[string]codec.Entry{}}, filters: []logpoller.Filter{}, reader: reader, } - if err := svc.init(cfg.Namespaces); err != nil { + if err := svc.bdRegistry.initAddressSharing(cfg.AddressShareGroups); err != nil { + return nil, err + } + + if err := svc.initNamespace(cfg.Namespaces); err != nil { return nil, err } @@ -84,9 +88,8 @@ func NewContractReaderService( svc.codec = svcCodec - svc.bindings.SetCodecs(svcCodec) + svc.bdRegistry.SetCodecs(svcCodec) svc.bindings.SetModifiers(svc.parsed.Modifiers) - return svc, nil } @@ -150,14 +153,14 @@ func (s *ContractReaderService) GetLatestValue(ctx context.Context, readIdentifi batch := []call{ { - ContractName: values.contract, - ReadName: values.genericName, - Params: params, - ReturnVal: returnVal, + Namespace: values.contract, + ReadName: values.genericName, + Params: params, + ReturnVal: returnVal, }, } - results, err := doMethodBatchCall(ctx, s.client, s.bindings, batch) + results, err := doMethodBatchCall(ctx, s.client, s.bdRegistry, batch) if err != nil { return err } @@ -184,15 +187,15 @@ func (s *ContractReaderService) BatchGetLatestValues(ctx context.Context, reques for idx, readReq := range req { idxLookup[bound][idx] = len(batch) batch = append(batch, call{ - ContractName: bound.Name, - ReadName: readReq.ReadName, - Params: readReq.Params, - ReturnVal: readReq.ReturnVal, + Namespace: bound.Name, + ReadName: readReq.ReadName, + Params: readReq.Params, + ReturnVal: readReq.ReturnVal, }) } } - results, err := doMethodBatchCall(ctx, s.client, s.bindings, batch) + results, err := doMethodBatchCall(ctx, s.client, s.bdRegistry, batch) if err != nil { return nil, err } @@ -255,21 +258,21 @@ func (s *ContractReaderService) QueryKey(ctx context.Context, contract types.Bou return sequenceOfValues, nil } -// Bind implements the types.ContractReader interface and allows new contract bindings to be added +// Bind implements the types.ContractReader interface and allows new contract namespaceBindings to be added // to the service. func (s *ContractReaderService) Bind(_ context.Context, bindings []types.BoundContract) error { - for _, binding := range bindings { - if err := s.bindings.Bind(binding); err != nil { + for i := range bindings { + if err := s.bdRegistry.Bind(&bindings[i]); err != nil { return err } - s.lookup.bindAddressForContract(binding.Name, binding.Address) + s.lookup.bindAddressForContract(bindings[i].Name, bindings[i].Address) } return nil } -// Unbind implements the types.ContractReader interface and allows existing contract bindings to be removed +// Unbind implements the types.ContractReader interface and allows existing contract namespaceBindings to be removed // from the service. func (s *ContractReaderService) Unbind(_ context.Context, bindings []types.BoundContract) error { for _, binding := range bindings { @@ -287,7 +290,7 @@ func (s *ContractReaderService) CreateContractType(readIdentifier string, forEnc return nil, fmt.Errorf("%w: no contract for read identifier", types.ErrInvalidConfig) } - return s.bindings.CreateType(values.contract, values.genericName, forEncoding) + return s.bdRegistry.CreateType(values.contract, values.genericName, forEncoding) } func (s *ContractReaderService) addCodecDef(forEncoding bool, namespace, genericName string, idl codec.IDL, idlDefinition interface{}, modCfg commoncodec.ModifiersConfig) error { @@ -309,7 +312,7 @@ func (s *ContractReaderService) addCodecDef(forEncoding bool, namespace, generic return nil } -func (s *ContractReaderService) init(namespaces map[string]config.ChainContractReader) error { +func (s *ContractReaderService) initNamespace(namespaces map[string]config.ChainContractReader) error { for namespace, nameSpaceDef := range namespaces { for genericName, read := range nameSpaceDef.Reads { utils.InjectAddressModifier(read.InputModifications, read.OutputModifications) @@ -381,7 +384,7 @@ func (s *ContractReaderService) addAccountRead(namespace string, genericName str return err } - s.bindings.AddReadBinding(namespace, genericName, reader) + s.bdRegistry.AddReadBinding(namespace, genericName, reader) return nil } diff --git a/pkg/solana/config/chain_reader.go b/pkg/solana/config/chain_reader.go index d7d4432a8..145ab9bfb 100644 --- a/pkg/solana/config/chain_reader.go +++ b/pkg/solana/config/chain_reader.go @@ -15,6 +15,12 @@ import ( type ContractReader struct { Namespaces map[string]ChainContractReader `json:"namespaces"` + // AddressShareGroups lists namespaces groups that share the same address. + // Whichever namespace or i.e. Binding from the list is Bound first will share that address with the rest of the group. + // Namespaces that were bound after the first one still have to be Bound to be initialised. + // If they are Bound with an empty address string, they will use the address of the first Bound contract. + // If they are Bound with a non-empty address string, an error will be thrown unless the address matches the address of the first Bound shared contract. + AddressShareGroups [][]string `json:"addressShareGroups,omitempty"` } type ChainContractReader struct { From 0754c3390119d8cd3489a8375baa7785fb7b7e4a Mon Sep 17 00:00:00 2001 From: ilija Date: Thu, 30 Jan 2025 13:41:28 +0100 Subject: [PATCH 2/8] Add CR cfg unmarshal validations for shared addresses --- pkg/solana/config/chain_reader.go | 30 +++++++++++ pkg/solana/config/chain_reader_test.go | 50 ++++++++++++++++--- ...json => test_contract_reader_invalid.json} | 0 ...ct_reader_invalid_address_share_group.json | 21 ++++++++ ...d.json => test_contract_reader_valid.json} | 0 ...act_reader_valid_address_share_groups.json | 21 ++++++++ ...ract_reader_valid_with_IDL_as_string.json} | 0 7 files changed, 115 insertions(+), 7 deletions(-) rename pkg/solana/config/{testChainReader_invalid.json => test_contract_reader_invalid.json} (100%) create mode 100644 pkg/solana/config/test_contract_reader_invalid_address_share_group.json rename pkg/solana/config/{testChainReader_valid.json => test_contract_reader_valid.json} (100%) create mode 100644 pkg/solana/config/test_contract_reader_valid_address_share_groups.json rename pkg/solana/config/{testChainReader_valid_with_IDL_as_string.json => test_contract_reader_valid_with_IDL_as_string.json} (100%) diff --git a/pkg/solana/config/chain_reader.go b/pkg/solana/config/chain_reader.go index 145ab9bfb..e3bfce500 100644 --- a/pkg/solana/config/chain_reader.go +++ b/pkg/solana/config/chain_reader.go @@ -68,6 +68,36 @@ type IndexedField struct { OnChainPath string `json:"onChainPath"` } +func (c *ContractReader) UnmarshalJSON(bytes []byte) error { + rawJSON := make(map[string]json.RawMessage) + if err := json.Unmarshal(bytes, &rawJSON); err != nil { + return err + } + + c.Namespaces = make(map[string]ChainContractReader) + if err := json.Unmarshal(rawJSON["namespaces"], &c.Namespaces); err != nil { + return err + } + + if err := json.Unmarshal(rawJSON["addressShareGroups"], &c.AddressShareGroups); err != nil { + return err + } + + if c.AddressShareGroups != nil { + seen := make(map[string][]string) + for _, group := range c.AddressShareGroups { + for _, namespace := range group { + if seenIn, alreadySeen := seen[namespace]; alreadySeen { + return fmt.Errorf("namespace %s is already in share group %v: %w", namespace, seenIn, commontypes.ErrInvalidConfig) + } + seen[namespace] = group + } + } + } + + return nil +} + func (c *ChainContractReader) UnmarshalJSON(bytes []byte) error { rawJSON := make(map[string]json.RawMessage) if err := json.Unmarshal(bytes, &rawJSON); err != nil { diff --git a/pkg/solana/config/chain_reader_test.go b/pkg/solana/config/chain_reader_test.go index cb52c56f9..42a3d6b17 100644 --- a/pkg/solana/config/chain_reader_test.go +++ b/pkg/solana/config/chain_reader_test.go @@ -10,25 +10,44 @@ import ( codeccommon "github.com/smartcontractkit/chainlink-common/pkg/codec" "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" ) -//go:embed testChainReader_valid.json +//go:embed test_contract_reader_invalid.json +var invalidJSON string + +//go:embed test_contract_reader_invalid_address_share_group.json +var invalidAddressShareGroupsJSON string + +//go:embed test_contract_reader_valid.json var validJSON string -//go:embed testChainReader_valid_with_IDL_as_string.json +//go:embed test_contract_reader_valid_with_IDL_as_string.json var validJSONWithIDLAsString string -//go:embed testChainReader_invalid.json -var invalidJSON string +//go:embed test_contract_reader_valid_address_share_groups.json +var validJsonWithAddressShareGroups string func TestChainReaderConfig(t *testing.T) { t.Parallel() + t.Run("invalid unmarshal", func(t *testing.T) { + t.Parallel() + + var result config.ContractReader + require.ErrorIs(t, json.Unmarshal([]byte(invalidJSON), &result), types.ErrInvalidConfig) + }) + + t.Run("invalid address share group unmarshal", func(t *testing.T) { + t.Parallel() + + var result config.ContractReader + require.ErrorIs(t, json.Unmarshal([]byte(invalidAddressShareGroupsJSON), &result), types.ErrInvalidConfig) + }) + t.Run("valid unmarshal with idl as struct", func(t *testing.T) { t.Parallel() @@ -38,22 +57,27 @@ func TestChainReaderConfig(t *testing.T) { }) t.Run("valid unmarshal with idl as string", func(t *testing.T) { + t.Parallel() + var result config.ContractReader require.NoError(t, json.Unmarshal([]byte(validJSONWithIDLAsString), &result)) assert.Equal(t, validChainReaderConfig, result) }) t.Run("valid unmarshal with PDA account", func(t *testing.T) { + t.Parallel() + 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.Run("valid unmarshal with address share groups", func(t *testing.T) { t.Parallel() var result config.ContractReader - require.ErrorIs(t, json.Unmarshal([]byte(invalidJSON), &result), types.ErrInvalidConfig) + require.NoError(t, json.Unmarshal([]byte(validJsonWithAddressShareGroups), &result)) + assert.Equal(t, validChainReaderConfigWithAddressShareGroups, result) }) t.Run("marshal", func(t *testing.T) { @@ -104,3 +128,15 @@ var validChainReaderConfig = config.ContractReader{ }, }, } + +var validChainReaderConfigWithAddressShareGroups = config.ContractReader{ + AddressShareGroups: [][]string{{"a", "b", "c"}, {"u", "v", "w"}}, + Namespaces: map[string]config.ChainContractReader{ + "Contract": { + IDL: nilIDL, + Reads: map[string]config.ReadDefinition{ + "Method": {}, + }, + }, + }, +} diff --git a/pkg/solana/config/testChainReader_invalid.json b/pkg/solana/config/test_contract_reader_invalid.json similarity index 100% rename from pkg/solana/config/testChainReader_invalid.json rename to pkg/solana/config/test_contract_reader_invalid.json diff --git a/pkg/solana/config/test_contract_reader_invalid_address_share_group.json b/pkg/solana/config/test_contract_reader_invalid_address_share_group.json new file mode 100644 index 000000000..82f5159fa --- /dev/null +++ b/pkg/solana/config/test_contract_reader_invalid_address_share_group.json @@ -0,0 +1,21 @@ +{ + "addressShareGroups": [["a","b","c"],["u","v","w","a"]], + "namespaces":{ + "Contract":{ + "anchorIDL":{ + "version":"0.1.0", + "name":"myProgram", + "accounts":[ + { + "name":"NilType", + "type":{ + "kind":"struct", + "fields":[] + } + } + ] + }, + "reads":{"Method":{}} + } + } +} \ No newline at end of file diff --git a/pkg/solana/config/testChainReader_valid.json b/pkg/solana/config/test_contract_reader_valid.json similarity index 100% rename from pkg/solana/config/testChainReader_valid.json rename to pkg/solana/config/test_contract_reader_valid.json diff --git a/pkg/solana/config/test_contract_reader_valid_address_share_groups.json b/pkg/solana/config/test_contract_reader_valid_address_share_groups.json new file mode 100644 index 000000000..7fbb18358 --- /dev/null +++ b/pkg/solana/config/test_contract_reader_valid_address_share_groups.json @@ -0,0 +1,21 @@ +{ + "addressShareGroups": [["a","b","c"],["u","v","w"]], + "namespaces":{ + "Contract":{ + "anchorIDL":{ + "version":"0.1.0", + "name":"myProgram", + "accounts":[ + { + "name":"NilType", + "type":{ + "kind":"struct", + "fields":[] + } + } + ] + }, + "reads":{"Method":{}} + } + } +} \ No newline at end of file diff --git a/pkg/solana/config/testChainReader_valid_with_IDL_as_string.json b/pkg/solana/config/test_contract_reader_valid_with_IDL_as_string.json similarity index 100% rename from pkg/solana/config/testChainReader_valid_with_IDL_as_string.json rename to pkg/solana/config/test_contract_reader_valid_with_IDL_as_string.json From 51b48f1887b34fa8cecafb3b341831a28cee0620 Mon Sep 17 00:00:00 2001 From: ilija Date: Thu, 30 Jan 2025 15:50:13 +0100 Subject: [PATCH 3/8] Fix CR binding registry init and address sharing err handling --- pkg/solana/chainreader/bindings.go | 6 ++---- pkg/solana/chainreader/chain_reader.go | 17 ++++++++++++----- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/pkg/solana/chainreader/bindings.go b/pkg/solana/chainreader/bindings.go index 1b24e868c..af25dd2b5 100644 --- a/pkg/solana/chainreader/bindings.go +++ b/pkg/solana/chainreader/bindings.go @@ -125,10 +125,8 @@ func (b *bindingsRegistry) handleAddressSharing(boundContract *types.BoundContra return err } shareGroup.address = key - } - - if boundContract.Address != shareGroupAddress.String() && boundContract.Address != "" { - return fmt.Errorf("binding: %q shares address: %q with namespaceBindings: %v and cannot be bound with a different address", boundContract.Name, shareGroup.address, shareGroup.group) + } else if boundContract.Address != shareGroupAddress.String() && boundContract.Address != "" { + return fmt.Errorf("namespace: %q shares address: %q with namespaceBindings: %v and cannot be bound with a new address: %s", boundContract.Name, shareGroupAddress, shareGroup.group, boundContract.Address) } boundContract.Address = shareGroupAddress.String() diff --git a/pkg/solana/chainreader/chain_reader.go b/pkg/solana/chainreader/chain_reader.go index 682827d62..97489fc7d 100644 --- a/pkg/solana/chainreader/chain_reader.go +++ b/pkg/solana/chainreader/chain_reader.go @@ -64,11 +64,17 @@ func NewContractReaderService( reader EventsReader, ) (*ContractReaderService, error) { svc := &ContractReaderService{ - lggr: logger.Named(lggr, ServiceName), - client: dataReader, - bdRegistry: bindingsRegistry{}, - lookup: newLookup(), - parsed: &codec.ParsedTypes{EncoderDefs: map[string]codec.Entry{}, DecoderDefs: map[string]codec.Entry{}}, + lggr: logger.Named(lggr, ServiceName), + client: dataReader, + bdRegistry: bindingsRegistry{ + namespaceBindings: make(map[string]readNameBindings), + addressShareGroups: make(map[string]*addressShareGroup), + }, + lookup: newLookup(), + parsed: &codec.ParsedTypes{ + EncoderDefs: map[string]codec.Entry{}, + DecoderDefs: map[string]codec.Entry{}, + }, filters: []logpoller.Filter{}, reader: reader, } @@ -261,6 +267,7 @@ func (s *ContractReaderService) QueryKey(ctx context.Context, contract types.Bou // Bind implements the types.ContractReader interface and allows new contract namespaceBindings to be added // to the service. func (s *ContractReaderService) Bind(_ context.Context, bindings []types.BoundContract) error { + fmt.Println("binidngs are ", bindings) for i := range bindings { if err := s.bdRegistry.Bind(&bindings[i]); err != nil { return err From bfd1bd8e676f38a575ef6cee207e00bf5ba7eb23 Mon Sep 17 00:00:00 2001 From: ilija Date: Thu, 30 Jan 2025 20:20:19 +0100 Subject: [PATCH 4/8] lint --- pkg/solana/config/chain_reader.go | 6 ++++-- pkg/solana/config/chain_reader_test.go | 7 +++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/pkg/solana/config/chain_reader.go b/pkg/solana/config/chain_reader.go index e3bfce500..a1b6839a5 100644 --- a/pkg/solana/config/chain_reader.go +++ b/pkg/solana/config/chain_reader.go @@ -79,8 +79,10 @@ func (c *ContractReader) UnmarshalJSON(bytes []byte) error { return err } - if err := json.Unmarshal(rawJSON["addressShareGroups"], &c.AddressShareGroups); err != nil { - return err + if rawJSON["addressShareGroups"] != nil { + if err := json.Unmarshal(rawJSON["addressShareGroups"], &c.AddressShareGroups); err != nil { + return err + } } if c.AddressShareGroups != nil { diff --git a/pkg/solana/config/chain_reader_test.go b/pkg/solana/config/chain_reader_test.go index 42a3d6b17..5afc1478c 100644 --- a/pkg/solana/config/chain_reader_test.go +++ b/pkg/solana/config/chain_reader_test.go @@ -10,6 +10,7 @@ import ( codeccommon "github.com/smartcontractkit/chainlink-common/pkg/codec" "github.com/smartcontractkit/chainlink-common/pkg/types" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/codec" "github.com/smartcontractkit/chainlink-solana/pkg/solana/codec/testutils" @@ -29,7 +30,7 @@ var validJSON string var validJSONWithIDLAsString string //go:embed test_contract_reader_valid_address_share_groups.json -var validJsonWithAddressShareGroups string +var validJSONWithAddressShareGroups string func TestChainReaderConfig(t *testing.T) { t.Parallel() @@ -76,7 +77,7 @@ func TestChainReaderConfig(t *testing.T) { t.Parallel() var result config.ContractReader - require.NoError(t, json.Unmarshal([]byte(validJsonWithAddressShareGroups), &result)) + require.NoError(t, json.Unmarshal([]byte(validJSONWithAddressShareGroups), &result)) assert.Equal(t, validChainReaderConfigWithAddressShareGroups, result) }) @@ -84,11 +85,9 @@ func TestChainReaderConfig(t *testing.T) { t.Parallel() result, err := json.Marshal(validChainReaderConfig) - require.NoError(t, err) var conf config.ContractReader - require.NoError(t, json.Unmarshal(result, &conf)) assert.Equal(t, validChainReaderConfig, conf) }) From cafa8107f7f605a36049c1e02becc24067df8410 Mon Sep 17 00:00:00 2001 From: ilija Date: Mon, 3 Feb 2025 12:20:25 +0100 Subject: [PATCH 5/8] Change CR shared address to handle empty address string in all CR methods --- .../relayinterface/chain_components_test.go | 140 ++++++++++++++---- pkg/solana/chainreader/bindings.go | 30 ++-- pkg/solana/chainreader/chain_reader.go | 5 +- 3 files changed, 137 insertions(+), 38 deletions(-) diff --git a/integration-tests/relayinterface/chain_components_test.go b/integration-tests/relayinterface/chain_components_test.go index a1afdcf67..f2152a34a 100644 --- a/integration-tests/relayinterface/chain_components_test.go +++ b/integration-tests/relayinterface/chain_components_test.go @@ -18,6 +18,7 @@ import ( "github.com/gagliardetto/solana-go/rpc" "github.com/gagliardetto/solana-go/rpc/ws" "github.com/gagliardetto/solana-go/text" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" commoncodec "github.com/smartcontractkit/chainlink-common/pkg/codec" @@ -37,6 +38,12 @@ import ( "github.com/smartcontractkit/chainlink-solana/pkg/solana/config" ) +const ( + AnyContractNameWithSharedAddress1 = AnyContractName + "Shared1" + AnyContractNameWithSharedAddress2 = AnyContractName + "Shared2" + AnyContractNameWithSharedAddress3 = AnyContractName + "Shared3" +) + func TestChainComponents(t *testing.T) { t.Parallel() helper := &helper{} @@ -95,7 +102,70 @@ func DisableTests(it *SolanaChainComponentsInterfaceTester[*testing.T]) { } func RunChainComponentsSolanaTests[T TestingT[T]](t T, it *SolanaChainComponentsInterfaceTester[T]) { - RunContractReaderSolanaTests(t, it) + testCases := Testcase[T]{ + Name: "Test address groups where first namespace shares address with second namespace", + Test: func(t T) { + ctx := tests.Context(t) + cfg := it.contractReaderConfig + cfg.AddressShareGroups = [][]string{{AnyContractNameWithSharedAddress1, AnyContractNameWithSharedAddress2, AnyContractNameWithSharedAddress3}} + cr := it.GetContractReaderWithCustomCfg(t, cfg) + + t.Run("Namespace is part of an address share group that doesn't have a registered address and provides no address during Bind", func(t T) { + bound1 := []types.BoundContract{{ + Name: AnyContractNameWithSharedAddress1, + }} + require.Error(t, cr.Bind(ctx, bound1)) + }) + + addressToBeShared := it.Helper.CreateAccount(t, AnyValueToReadWithoutAnArgument).String() + t.Run("Namespace is part of an address share group that doesn't have a registered address and provides an address during Bind", func(t T) { + bound1 := []types.BoundContract{{Name: AnyContractNameWithSharedAddress1, Address: addressToBeShared}} + + require.NoError(t, cr.Bind(ctx, bound1)) + + var prim uint64 + require.NoError(t, cr.GetLatestValue(ctx, bound1[0].ReadIdentifier(MethodReturningUint64), primitives.Unconfirmed, nil, &prim)) + assert.Equal(t, AnyValueToReadWithoutAnArgument, prim) + }) + + t.Run("Namespace is part of an address share group that has a registered address and provides that same address during Bind", func(t T) { + bound2 := []types.BoundContract{{ + Name: AnyContractNameWithSharedAddress2, + Address: addressToBeShared}} + require.NoError(t, cr.Bind(ctx, bound2)) + + var prim uint64 + require.NoError(t, cr.GetLatestValue(ctx, bound2[0].ReadIdentifier(MethodReturningUint64), primitives.Unconfirmed, nil, &prim)) + assert.Equal(t, AnyValueToReadWithoutAnArgument, prim) + assert.Equal(t, addressToBeShared, bound2[0].Address) + }) + + t.Run("Namespace is part of an address share group that has a registered address and provides no address during Bind", func(t T) { + bound3 := []types.BoundContract{{Name: AnyContractNameWithSharedAddress3}} + require.NoError(t, cr.Bind(ctx, bound3)) + + var prim uint64 + require.NoError(t, cr.GetLatestValue(ctx, bound3[0].ReadIdentifier(MethodReturningUint64), primitives.Unconfirmed, nil, &prim)) + assert.Equal(t, AnyValueToReadWithoutAnArgument, prim) + assert.Equal(t, addressToBeShared, bound3[0].Address) + + // when run in a loop Bind address won't be set, so check if CR Method works without set address. + prim = 0 + require.NoError(t, cr.GetLatestValue(ctx, types.BoundContract{ + Address: "", + Name: AnyContractNameWithSharedAddress3, + }.ReadIdentifier(MethodReturningUint64), primitives.Unconfirmed, nil, &prim)) + assert.Equal(t, AnyValueToReadWithoutAnArgument, prim) + }) + + t.Run("Namespace is not part of an address share group that has a registered address and provides no address during Bind", func(t T) { + require.Error(t, cr.Bind(ctx, []types.BoundContract{{Name: AnyContractName}})) + }) + }, + } + + RunTests(t, it, []Testcase[T]{testCases}) + RunContractReaderTests(t, it) // Add ChainWriter tests here } @@ -104,20 +174,12 @@ func RunChainComponentsInLoopSolanaTests[T TestingT[T]](t T, it ChainComponentsI // Add ChainWriter tests here } -func RunContractReaderSolanaTests[T TestingT[T]](t T, it *SolanaChainComponentsInterfaceTester[T]) { +func RunContractReaderTests[T TestingT[T]](t T, it *SolanaChainComponentsInterfaceTester[T]) { RunContractReaderInterfaceTests(t, it, false, true) - - var testCases []Testcase[T] - - RunTests(t, it, testCases) } func RunContractReaderInLoopTests[T TestingT[T]](t T, it ChainComponentsInterfaceTester[T]) { RunContractReaderInterfaceTests(t, it, false, true) - - var testCases []Testcase[T] - - RunTests(t, it, testCases) } type SolanaChainComponentsInterfaceTesterHelper[T TestingT[T]] interface { @@ -139,26 +201,28 @@ type SolanaChainComponentsInterfaceTester[T TestingT[T]] struct { func (it *SolanaChainComponentsInterfaceTester[T]) Setup(t T) { t.Cleanup(func() {}) - it.contractReaderConfig = config.ContractReader{ - Namespaces: map[string]config.ChainContractReader{ - AnyContractName: { - IDL: mustUnmarshalIDL(t, string(it.Helper.GetJSONEncodedIDL(t))), - Reads: map[string]config.ReadDefinition{ - MethodReturningUint64: { - ChainSpecificName: "DataAccount", - ReadType: config.Account, - OutputModifications: commoncodec.ModifiersConfig{ - &commoncodec.PropertyExtractorConfig{FieldName: "U64Value"}, - }, - }, - MethodReturningUint64Slice: { - ChainSpecificName: "DataAccount", - OutputModifications: commoncodec.ModifiersConfig{ - &commoncodec.PropertyExtractorConfig{FieldName: "U64Slice"}, - }, - }, + anyContractReadDef := config.ChainContractReader{ + IDL: mustUnmarshalIDL(t, string(it.Helper.GetJSONEncodedIDL(t))), + Reads: map[string]config.ReadDefinition{ + MethodReturningUint64: { + ChainSpecificName: "DataAccount", + ReadType: config.Account, + OutputModifications: commoncodec.ModifiersConfig{ + &commoncodec.PropertyExtractorConfig{FieldName: "U64Value"}, + }, + }, + MethodReturningUint64Slice: { + ChainSpecificName: "DataAccount", + OutputModifications: commoncodec.ModifiersConfig{ + &commoncodec.PropertyExtractorConfig{FieldName: "U64Slice"}, }, }, + }, + } + + it.contractReaderConfig = config.ContractReader{ + Namespaces: map[string]config.ChainContractReader{ + AnyContractName: anyContractReadDef, AnySecondContractName: { IDL: mustUnmarshalIDL(t, string(it.Helper.GetJSONEncodedIDL(t))), Reads: map[string]config.ReadDefinition{ @@ -170,6 +234,10 @@ func (it *SolanaChainComponentsInterfaceTester[T]) Setup(t T) { }, }, }, + // these are for testing shared address groups + AnyContractNameWithSharedAddress1: anyContractReadDef, + AnyContractNameWithSharedAddress2: anyContractReadDef, + AnyContractNameWithSharedAddress3: anyContractReadDef, }, } } @@ -208,6 +276,22 @@ func (it *SolanaChainComponentsInterfaceTester[T]) GetContractReader(t T) types. return svc } +func (it *SolanaChainComponentsInterfaceTester[T]) GetContractReaderWithCustomCfg(t T, cfg config.ContractReader) types.ContractReader { + ctx := it.Helper.Context(t) + if it.cr != nil { + return it.cr + } + + svc, err := chainreader.NewChainReaderService(it.Helper.Logger(t), it.Helper.RPCClient(), cfg) + + require.NoError(t, err) + require.NoError(t, svc.Start(ctx)) + + it.cr = svc + + return svc +} + func (it *SolanaChainComponentsInterfaceTester[T]) GetContractWriter(t T) types.ContractWriter { return nil } diff --git a/pkg/solana/chainreader/bindings.go b/pkg/solana/chainreader/bindings.go index af25dd2b5..667d54b32 100644 --- a/pkg/solana/chainreader/bindings.go +++ b/pkg/solana/chainreader/bindings.go @@ -70,6 +70,10 @@ func (b *bindingsRegistry) CreateType(namespace, readName string, forEncoding bo } func (b *bindingsRegistry) Bind(boundContract *types.BoundContract) error { + if boundContract == nil { + return fmt.Errorf("%w: bound contract is nil", types.ErrInvalidType) + } + if err := b.handleAddressSharing(boundContract); err != nil { return err } @@ -81,7 +85,7 @@ func (b *bindingsRegistry) Bind(boundContract *types.BoundContract) error { key, err := solana.PublicKeyFromBase58(boundContract.Address) if err != nil { - return err + return fmt.Errorf("%w: failed to parse address: %q for contract %q", types.ErrInvalidConfig, boundContract.Address, boundContract.Name) } for _, rBinding := range rBindings { @@ -109,8 +113,8 @@ func (b *bindingsRegistry) SetModifiers(modifier commoncodec.Modifier) { func (b *bindingsRegistry) handleAddressSharing(boundContract *types.BoundContract) error { - shareGroup, sharesAddress := b.addressShareGroups[boundContract.Name] - if !sharesAddress { + shareGroup, isInAGroup := b.getShareGroup(*boundContract) + if !isInAGroup { return nil } @@ -118,21 +122,29 @@ func (b *bindingsRegistry) handleAddressSharing(boundContract *types.BoundContra defer shareGroup.mux.Unlock() // set shared address to the binding address - shareGroupAddress := shareGroup.address - if shareGroupAddress.IsZero() { + if shareGroup.address.IsZero() { key, err := solana.PublicKeyFromBase58(boundContract.Address) if err != nil { return err } - shareGroup.address = key - } else if boundContract.Address != shareGroupAddress.String() && boundContract.Address != "" { - return fmt.Errorf("namespace: %q shares address: %q with namespaceBindings: %v and cannot be bound with a new address: %s", boundContract.Name, shareGroupAddress, shareGroup.group, boundContract.Address) + b.addressShareGroups[boundContract.Name].address, shareGroup.address = key, key + } else if boundContract.Address != shareGroup.address.String() && boundContract.Address != "" { + return fmt.Errorf("namespace: %q shares address: %q with namespaceBindings: %v and cannot be bound with a new address: %s", boundContract.Name, shareGroup.address, shareGroup.group, boundContract.Address) } - boundContract.Address = shareGroupAddress.String() + boundContract.Address = shareGroup.address.String() return nil } +func (b *bindingsRegistry) getShareGroup(boundContract types.BoundContract) (*addressShareGroup, bool) { + shareGroup, sharesAddress := b.addressShareGroups[boundContract.Name] + if !sharesAddress { + return nil, false + } + + return shareGroup, sharesAddress +} + func (b *bindingsRegistry) initAddressSharing(addressShareGroups [][]string) error { b.addressShareGroups = make(map[string]*addressShareGroup) for _, group := range addressShareGroups { diff --git a/pkg/solana/chainreader/chain_reader.go b/pkg/solana/chainreader/chain_reader.go index 97489fc7d..24ff0ab5d 100644 --- a/pkg/solana/chainreader/chain_reader.go +++ b/pkg/solana/chainreader/chain_reader.go @@ -267,13 +267,16 @@ func (s *ContractReaderService) QueryKey(ctx context.Context, contract types.Bou // Bind implements the types.ContractReader interface and allows new contract namespaceBindings to be added // to the service. func (s *ContractReaderService) Bind(_ context.Context, bindings []types.BoundContract) error { - fmt.Println("binidngs are ", bindings) for i := range bindings { if err := s.bdRegistry.Bind(&bindings[i]); err != nil { return err } s.lookup.bindAddressForContract(bindings[i].Name, bindings[i].Address) + // also bind with an empty address so that we can look up the contract without providing address when calling CR methods + if _, isInAShareGroup := s.bdRegistry.getShareGroup(bindings[i]); isInAShareGroup { + s.lookup.bindAddressForContract(bindings[i].Name, "") + } } return nil From 51bfe92f9512b88fe35b700c28d7e00ff86459ae Mon Sep 17 00:00:00 2001 From: ilija Date: Fri, 7 Feb 2025 19:35:12 +0100 Subject: [PATCH 6/8] Fix rebase conflicts --- .../relayinterface/chain_components_test.go | 2 +- pkg/solana/chainreader/chain_reader.go | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/integration-tests/relayinterface/chain_components_test.go b/integration-tests/relayinterface/chain_components_test.go index f2152a34a..af08521b9 100644 --- a/integration-tests/relayinterface/chain_components_test.go +++ b/integration-tests/relayinterface/chain_components_test.go @@ -282,7 +282,7 @@ func (it *SolanaChainComponentsInterfaceTester[T]) GetContractReaderWithCustomCf return it.cr } - svc, err := chainreader.NewChainReaderService(it.Helper.Logger(t), it.Helper.RPCClient(), cfg) + svc, err := chainreader.NewContractReaderService(it.Helper.Logger(t), it.Helper.RPCClient(), cfg, nil) require.NoError(t, err) require.NoError(t, svc.Start(ctx)) diff --git a/pkg/solana/chainreader/chain_reader.go b/pkg/solana/chainreader/chain_reader.go index 24ff0ab5d..4eb17ef44 100644 --- a/pkg/solana/chainreader/chain_reader.go +++ b/pkg/solana/chainreader/chain_reader.go @@ -44,7 +44,7 @@ type ContractReaderService struct { lookup *lookup parsed *codec.ParsedTypes codec types.RemoteCodec - filters []logpoller.Filter + filters []logpoller.Filter // service state management wg sync.WaitGroup @@ -75,8 +75,8 @@ func NewContractReaderService( EncoderDefs: map[string]codec.Entry{}, DecoderDefs: map[string]codec.Entry{}, }, - filters: []logpoller.Filter{}, - reader: reader, + filters: []logpoller.Filter{}, + reader: reader, } if err := svc.bdRegistry.initAddressSharing(cfg.AddressShareGroups); err != nil { @@ -95,7 +95,7 @@ func NewContractReaderService( svc.codec = svcCodec svc.bdRegistry.SetCodecs(svcCodec) - svc.bindings.SetModifiers(svc.parsed.Modifiers) + svc.bdRegistry.SetModifiers(svc.parsed.Modifiers) return svc, nil } @@ -228,7 +228,7 @@ func (s *ContractReaderService) BatchGetLatestValues(ctx context.Context, reques // QueryKey implements the types.ContractReader interface. func (s *ContractReaderService) QueryKey(ctx context.Context, contract types.BoundContract, filter query.KeyFilter, limitAndSort query.LimitAndSort, sequenceDataType any) ([]types.Sequence, error) { - binding, err := s.bindings.GetReadBinding(contract.Name, filter.Key) + binding, err := s.bdRegistry.GetReadBinding(contract.Name, filter.Key) if err != nil { return nil, err } @@ -418,7 +418,7 @@ func (s *ContractReaderService) addEventRead( filter := toLPFilter(readDefinition.PollingFilter, contractAddress, subKeys[:]) s.filters = append(s.filters, filter) - s.bindings.AddReadBinding(namespace, genericName, newEventReadBinding( + s.bdRegistry.AddReadBinding(namespace, genericName, newEventReadBinding( namespace, genericName, mappedTuples, From 2162f0131be05b79c532fd4349ca4986e6d5175a Mon Sep 17 00:00:00 2001 From: ilija Date: Fri, 7 Feb 2025 19:40:58 +0100 Subject: [PATCH 7/8] goimports --- pkg/solana/chainreader/bindings.go | 1 - 1 file changed, 1 deletion(-) diff --git a/pkg/solana/chainreader/bindings.go b/pkg/solana/chainreader/bindings.go index 667d54b32..b24a4191f 100644 --- a/pkg/solana/chainreader/bindings.go +++ b/pkg/solana/chainreader/bindings.go @@ -111,7 +111,6 @@ func (b *bindingsRegistry) SetModifiers(modifier commoncodec.Modifier) { } } - func (b *bindingsRegistry) handleAddressSharing(boundContract *types.BoundContract) error { shareGroup, isInAGroup := b.getShareGroup(*boundContract) if !isInAGroup { From b07d057afa7b0fdcd576a5870ea22f417dc28be3 Mon Sep 17 00:00:00 2001 From: ilija Date: Fri, 7 Feb 2025 22:15:19 +0100 Subject: [PATCH 8/8] Add test case for address sharing --- .../relayinterface/chain_components_test.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/integration-tests/relayinterface/chain_components_test.go b/integration-tests/relayinterface/chain_components_test.go index af08521b9..7aa896cc2 100644 --- a/integration-tests/relayinterface/chain_components_test.go +++ b/integration-tests/relayinterface/chain_components_test.go @@ -140,6 +140,16 @@ func RunChainComponentsSolanaTests[T TestingT[T]](t T, it *SolanaChainComponents assert.Equal(t, addressToBeShared, bound2[0].Address) }) + t.Run("Namespace is part of an address share group that has a registered address and provides a wrong address during Bind", func(t T) { + key, err := solana.NewRandomPrivateKey() + require.NoError(t, err) + + bound2 := []types.BoundContract{{ + Name: AnyContractNameWithSharedAddress2, + Address: key.PublicKey().String()}} + require.Error(t, cr.Bind(ctx, bound2)) + }) + t.Run("Namespace is part of an address share group that has a registered address and provides no address during Bind", func(t T) { bound3 := []types.BoundContract{{Name: AnyContractNameWithSharedAddress3}} require.NoError(t, cr.Bind(ctx, bound3))