From 0e5b4a0428c8e815fd0417c3cd6efee2d79b5c99 Mon Sep 17 00:00:00 2001 From: Gustavo Gama Date: Tue, 5 May 2026 20:23:09 -0300 Subject: [PATCH] feat: add catalog create address refs changeset --- catalog/changesets/create_address_ref.go | 68 +++++++ catalog/changesets/create_address_ref_test.go | 181 ++++++++++++++++++ catalog/operations/create_address_ref.go | 51 +++++ 3 files changed, 300 insertions(+) create mode 100644 catalog/changesets/create_address_ref.go create mode 100644 catalog/changesets/create_address_ref_test.go create mode 100644 catalog/operations/create_address_ref.go diff --git a/catalog/changesets/create_address_ref.go b/catalog/changesets/create_address_ref.go new file mode 100644 index 0000000..9bc39e2 --- /dev/null +++ b/catalog/changesets/create_address_ref.go @@ -0,0 +1,68 @@ +package changesets + +import ( + "errors" + "fmt" + + "github.com/samber/lo" + cldfdatastore "github.com/smartcontractkit/chainlink-deployments-framework/datastore" + cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" + cldfops "github.com/smartcontractkit/chainlink-deployments-framework/operations" + + "github.com/smartcontractkit/cld-changesets/catalog/operations" +) + +// CreateAddressRefChangeset creates address ref entries in the Catalog service. +type CreateAddressRefChangeset struct{} + +type CreateAddressRefChangesetInput struct { + AddressRefs []cldfdatastore.AddressRef `json:"addressRefs"` +} + +// VerifyPreconditions ensures the input is valid. +func (CreateAddressRefChangeset) VerifyPreconditions(e cldf.Environment, input CreateAddressRefChangesetInput) error { + if len(input.AddressRefs) == 0 { + return errors.New("missing address refs input") + } + if e.DataStore == nil { + return errors.New("missing datastore in environment") + } + + uniqAddressRefs := lo.UniqBy(input.AddressRefs, func(ar cldfdatastore.AddressRef) cldfdatastore.AddressRefKey { + return ar.Key() + }) + if len(uniqAddressRefs) != len(input.AddressRefs) { + return errors.New("duplicate address ref entries found in input") + } + + for _, addressRef := range input.AddressRefs { + _, err := e.DataStore.Addresses().Get(addressRef.Key()) + if err == nil { + return fmt.Errorf("address ref for chain selector %v, type %v, version %v and qualifier %q already exists", + addressRef.ChainSelector, addressRef.Type, addressRef.Version, addressRef.Qualifier) + } + if !errors.Is(err, cldfdatastore.ErrAddressRefNotFound) { + return fmt.Errorf("failed to retrieve address ref for chain selector %v, type %v, version %v and qualifier %q: %w", + addressRef.ChainSelector, addressRef.Type, addressRef.Version, addressRef.Qualifier, err) + } + } + + return nil +} + +// Apply executes the changeset, adding the address refs to the Catalog service. +func (CreateAddressRefChangeset) Apply(e cldf.Environment, input CreateAddressRefChangesetInput) (cldf.ChangesetOutput, error) { + deps := operations.CreateAddressRefDeps{DataStore: e.DataStore} + opInput := operations.CreateAddressRefInput{AddressRefs: input.AddressRefs} + + report, err := cldfops.ExecuteOperation(e.OperationsBundle, operations.CreateAddressRefOp, deps, opInput) + out := cldf.ChangesetOutput{ + DataStore: report.Output.DataStore, + Reports: []cldfops.Report[any, any]{report.ToGenericReport()}, + } + if err != nil { + return out, err + } + + return out, nil +} diff --git a/catalog/changesets/create_address_ref_test.go b/catalog/changesets/create_address_ref_test.go new file mode 100644 index 0000000..1f5726f --- /dev/null +++ b/catalog/changesets/create_address_ref_test.go @@ -0,0 +1,181 @@ +package changesets + +import ( + "testing" + + "github.com/Masterminds/semver/v3" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/stretchr/testify/require" + + cldfdatastore "github.com/smartcontractkit/chainlink-deployments-framework/datastore" + cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" + cldfoperations "github.com/smartcontractkit/chainlink-deployments-framework/operations" + cldflogger "github.com/smartcontractkit/chainlink-deployments-framework/pkg/logger" + + "github.com/smartcontractkit/cld-changesets/catalog/operations" +) + +func TestCreateAddressRefChangeset_VerifyPreconditions(t *testing.T) { + t.Parallel() + + version := semver.MustParse("1.0.0") + addressRef1 := cldfdatastore.AddressRef{Address: "0x01", ChainSelector: 1234, Type: "MyContract", Version: version, Qualifier: "q1"} + addressRef2 := cldfdatastore.AddressRef{Address: "0x02", ChainSelector: 1234, Type: "MyContract", Version: version, Qualifier: "q1"} + + tests := []struct { + name string + env cldf.Environment + input CreateAddressRefChangesetInput + wantErr string + }{ + { + name: "success: valid preconditions", + env: cldf.Environment{DataStore: cldfdatastore.NewMemoryDataStore().Seal()}, + input: CreateAddressRefChangesetInput{ + AddressRefs: []cldfdatastore.AddressRef{addressRef1}, + }, + }, + { + name: "failure: missing datastore", + env: cldf.Environment{}, + input: CreateAddressRefChangesetInput{ + AddressRefs: []cldfdatastore.AddressRef{addressRef1}, + }, + wantErr: "missing datastore in environment", + }, + { + name: "failure: no address refs given", + env: cldf.Environment{DataStore: cldfdatastore.NewMemoryDataStore().Seal()}, + input: CreateAddressRefChangesetInput{ + AddressRefs: []cldfdatastore.AddressRef{}, + }, + wantErr: "missing address refs input", + }, + { + name: "failure: duplicate entries", + env: cldf.Environment{DataStore: cldfdatastore.NewMemoryDataStore().Seal()}, + input: CreateAddressRefChangesetInput{ + AddressRefs: []cldfdatastore.AddressRef{addressRef1, addressRef2}, + }, + wantErr: "duplicate address ref entries found in input", + }, + { + name: "failure: address ref already exists", + env: cldf.Environment{DataStore: func() cldfdatastore.DataStore { + ds := cldfdatastore.NewMemoryDataStore() + err := ds.Addresses().Add(addressRef1) + require.NoError(t, err) + + return ds.Seal() + }()}, + input: CreateAddressRefChangesetInput{ + AddressRefs: []cldfdatastore.AddressRef{addressRef1}, + }, + wantErr: "address ref for chain selector 1234, type MyContract, version 1.0.0 and qualifier \"q1\" already exists", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + err := CreateAddressRefChangeset{}.VerifyPreconditions(tt.env, tt.input) + + if tt.wantErr == "" { + require.NoError(t, err) + } else { + require.ErrorContains(t, err, tt.wantErr) + } + }) + } +} + +func TestCreateAddressRefChangeset_Apply(t *testing.T) { + t.Parallel() + + version := semver.MustParse("1.0.0") + addressRef1 := cldfdatastore.AddressRef{Address: "0x01", ChainSelector: 1234, Type: "MyContract", Version: version, Qualifier: "q1"} + addressRef2 := cldfdatastore.AddressRef{Address: "0x02", ChainSelector: 1234, Type: "MyContract", Version: version, Qualifier: "q2"} + + tests := []struct { + name string + env cldf.Environment + input CreateAddressRefChangesetInput + want cldf.ChangesetOutput + wantErr string + }{ + { + name: "success: adds two entries to address refs", + env: cldf.Environment{ + DataStore: testDataStoreWithAddressRefs(t).Seal(), + OperationsBundle: cldfoperations.NewBundle(t.Context, cldflogger.Test(t), cldfoperations.NewMemoryReporter()), + }, + input: CreateAddressRefChangesetInput{ + AddressRefs: []cldfdatastore.AddressRef{addressRef1, addressRef2}, + }, + want: cldf.ChangesetOutput{ + DataStore: testDataStoreWithAddressRefs(t, addressRef1, addressRef2), + Reports: []cldfoperations.Report[any, any]{{ + Def: cldfoperations.Definition{ + ID: "catalog-create-address-ref", + Version: semver.MustParse("1.0.0"), + Description: "Add address ref entries to the Catalog service", + }, + Input: operations.CreateAddressRefInput{ + AddressRefs: []cldfdatastore.AddressRef{addressRef1, addressRef2}, + }, + Output: operations.CreateAddressRefOutput{ + DataStore: testDataStoreWithAddressRefs(t, addressRef1, addressRef2), + }, + }}, + }, + }, + { + name: "failure: fails to add second entry", + env: cldf.Environment{ + DataStore: testDataStoreWithAddressRefs(t, addressRef2).Seal(), + OperationsBundle: cldfoperations.NewBundle(t.Context, cldflogger.Test(t), cldfoperations.NewMemoryReporter()), + }, + input: CreateAddressRefChangesetInput{ + AddressRefs: []cldfdatastore.AddressRef{addressRef1, addressRef2}, + }, + wantErr: "failed to create address ref entry 1 in catalog store: " + + "an address ref with the supplied key already exists", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + got, err := CreateAddressRefChangeset{}.Apply(tt.env, tt.input) + + if tt.wantErr == "" { + require.NoError(t, err) + require.Empty(t, + cmp.Diff(tt.want, got, + cmpopts.IgnoreFields(cldfoperations.Report[any, any]{}, "ID", "Timestamp"), + cmpopts.IgnoreUnexported(cldfdatastore.MemoryAddressRefStore{}, cldfdatastore.MemoryChainMetadataStore{}, + cldfdatastore.MemoryContractMetadataStore{}, cldfdatastore.MemoryEnvMetadataStore{}, + cldfdatastore.LabelSet{}))) + } else { + require.ErrorContains(t, err, tt.wantErr) + } + }) + } +} + +// ----- helpers ----- + +func testDataStoreWithAddressRefs( + t *testing.T, addressRefs ...cldfdatastore.AddressRef, +) cldfdatastore.MutableDataStore { + t.Helper() + + ds := cldfdatastore.NewMemoryDataStore() + for _, ar := range addressRefs { + err := ds.Addresses().Add(ar) + require.NoError(t, err) + } + + return ds +} diff --git a/catalog/operations/create_address_ref.go b/catalog/operations/create_address_ref.go new file mode 100644 index 0000000..eff947a --- /dev/null +++ b/catalog/operations/create_address_ref.go @@ -0,0 +1,51 @@ +package operations + +import ( + "fmt" + + "github.com/Masterminds/semver/v3" + + cldfdatastore "github.com/smartcontractkit/chainlink-deployments-framework/datastore" + cldfops "github.com/smartcontractkit/chainlink-deployments-framework/operations" +) + +// CreateAddressRefDeps holds non-serializable dependencies for the +// CreateAddressRefOp operation. +type CreateAddressRefDeps struct { + DataStore cldfdatastore.DataStore +} + +// CreateAddressRefInput is the serializable input of a CreateAddressRefOp invocation. +type CreateAddressRefInput struct { + AddressRefs []cldfdatastore.AddressRef +} + +// CreateAddressRefOutput is the serializable output of a CreateAddressRefOp invocation. +type CreateAddressRefOutput struct { + DataStore cldfdatastore.MutableDataStore +} + +// CreateAddressRefOp creates address ref entries in the Catalog service. +var CreateAddressRefOp = cldfops.NewOperation( + "catalog-create-address-ref", + semver.MustParse("1.0.0"), + "Add address ref entries to the Catalog service", + func(b cldfops.Bundle, deps CreateAddressRefDeps, input CreateAddressRefInput) (CreateAddressRefOutput, error) { + dataStore := cldfdatastore.NewMemoryDataStore() + err := dataStore.Merge(deps.DataStore) + if err != nil { + return CreateAddressRefOutput{}, fmt.Errorf("failed to create memory data store: %w", err) + } + + for i, item := range input.AddressRefs { + err = dataStore.Addresses().Add(item) + if err != nil { + return CreateAddressRefOutput{}, fmt.Errorf("failed to create address ref entry %d in catalog store: %w", i, err) + } + } + + b.Logger.Infow("Catalog AddressRef created successfully") + + return CreateAddressRefOutput{DataStore: dataStore}, nil + }, +)