diff --git a/catalog/changesets/create_contract_metadata.go b/catalog/changesets/create_contract_metadata.go new file mode 100644 index 0000000..699708d --- /dev/null +++ b/catalog/changesets/create_contract_metadata.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" +) + +// CreateContractMetadataChangeset creates contract metadata entries in the Catalog service. +type CreateContractMetadataChangeset struct{} + +type CreateContractMetadataChangesetInput struct { + ContractMetadata []cldfdatastore.ContractMetadata `json:"contractMetadata"` +} + +// VerifyPreconditions ensures the input is valid. +func (CreateContractMetadataChangeset) VerifyPreconditions(e cldf.Environment, input CreateContractMetadataChangesetInput) error { + if len(input.ContractMetadata) == 0 { + return errors.New("missing contract metadata input") + } + if e.DataStore == nil { + return errors.New("missing datastore in environment") + } + + uniqContractMetadata := lo.UniqBy(input.ContractMetadata, func(cm cldfdatastore.ContractMetadata) cldfdatastore.ContractMetadataKey { + return cm.Key() + }) + if len(uniqContractMetadata) != len(input.ContractMetadata) { + return errors.New("duplicate contract metadata entries found in input") + } + + for _, contractMetadata := range input.ContractMetadata { + _, err := e.DataStore.ContractMetadata().Get(contractMetadata.Key()) + if err == nil { + return fmt.Errorf("contract metadata for chain selector %v and address %v already exists", + contractMetadata.ChainSelector, contractMetadata.Address) + } + if !errors.Is(err, cldfdatastore.ErrContractMetadataNotFound) { + return fmt.Errorf("failed to retrieve contract metadata for chain selector %v and address %v: %w", + contractMetadata.ChainSelector, contractMetadata.Address, err) + } + } + + return nil +} + +// Apply executes the changeset, adding the contract metadata to the Catalog service. +func (CreateContractMetadataChangeset) Apply(e cldf.Environment, input CreateContractMetadataChangesetInput) (cldf.ChangesetOutput, error) { + deps := operations.CreateContractMetadataDeps{DataStore: e.DataStore} + opInput := operations.CreateContractMetadataInput{ContractMetadata: input.ContractMetadata} + + report, err := cldfops.ExecuteOperation(e.OperationsBundle, operations.CreateContractMetadataOp, 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_contract_metadata_test.go b/catalog/changesets/create_contract_metadata_test.go new file mode 100644 index 0000000..57201e4 --- /dev/null +++ b/catalog/changesets/create_contract_metadata_test.go @@ -0,0 +1,178 @@ +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 TestCreateContractMetadataChangeset_VerifyPreconditions(t *testing.T) { + t.Parallel() + + contractMetadata1 := cldfdatastore.ContractMetadata{Address: "0x01", ChainSelector: 1234, Metadata: "value1"} + contractMetadata2 := cldfdatastore.ContractMetadata{Address: "0x01", ChainSelector: 1234, Metadata: "value2"} + + tests := []struct { + name string + env cldf.Environment + input CreateContractMetadataChangesetInput + wantErr string + }{ + { + name: "success: valid preconditions", + env: cldf.Environment{DataStore: cldfdatastore.NewMemoryDataStore().Seal()}, + input: CreateContractMetadataChangesetInput{ + ContractMetadata: []cldfdatastore.ContractMetadata{contractMetadata1}, + }, + }, + { + name: "failure: missing datastore", + env: cldf.Environment{}, + input: CreateContractMetadataChangesetInput{ + ContractMetadata: []cldfdatastore.ContractMetadata{{}}, + }, + wantErr: "missing datastore in environment", + }, + { + name: "failure: no contract metadata given", + env: cldf.Environment{DataStore: cldfdatastore.NewMemoryDataStore().Seal()}, + input: CreateContractMetadataChangesetInput{ + ContractMetadata: []cldfdatastore.ContractMetadata{}, + }, + wantErr: "missing contract metadata input", + }, + { + name: "failure: duplicate entries", + env: cldf.Environment{DataStore: cldfdatastore.NewMemoryDataStore().Seal()}, + input: CreateContractMetadataChangesetInput{ + ContractMetadata: []cldfdatastore.ContractMetadata{contractMetadata1, contractMetadata2}, + }, + wantErr: "duplicate contract metadata entries found in input", + }, + { + name: "failure: contract metadata already exists", + env: cldf.Environment{DataStore: func() cldfdatastore.DataStore { + ds := cldfdatastore.NewMemoryDataStore() + err := ds.ContractMetadata().Add(contractMetadata1) + require.NoError(t, err) + + return ds.Seal() + }()}, + input: CreateContractMetadataChangesetInput{ + ContractMetadata: []cldfdatastore.ContractMetadata{contractMetadata1}, + }, + wantErr: "contract metadata for chain selector 1234 and address 0x01 already exists", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + err := CreateContractMetadataChangeset{}.VerifyPreconditions(tt.env, tt.input) + + if tt.wantErr == "" { + require.NoError(t, err) + } else { + require.ErrorContains(t, err, tt.wantErr) + } + }) + } +} + +func TestCreateContractMetadataChangeset_Apply(t *testing.T) { + t.Parallel() + + contractMetadata1 := cldfdatastore.ContractMetadata{Address: "0x01", ChainSelector: 1234, Metadata: "value1"} + contractMetadata2 := cldfdatastore.ContractMetadata{Address: "0x02", ChainSelector: 1234, Metadata: "value2"} + + tests := []struct { + name string + env cldf.Environment + input CreateContractMetadataChangesetInput + want cldf.ChangesetOutput + wantErr string + }{ + { + name: "success: adds two entries to contract metadata", + env: cldf.Environment{ + DataStore: testDataStoreWithContractMetadata(t).Seal(), + OperationsBundle: cldfoperations.NewBundle(t.Context, cldflogger.Test(t), cldfoperations.NewMemoryReporter()), + }, + input: CreateContractMetadataChangesetInput{ + ContractMetadata: []cldfdatastore.ContractMetadata{contractMetadata1, contractMetadata2}, + }, + want: cldf.ChangesetOutput{ + DataStore: testDataStoreWithContractMetadata(t, contractMetadata1, contractMetadata2), + Reports: []cldfoperations.Report[any, any]{{ + Def: cldfoperations.Definition{ + ID: "catalog-create-contract-metadata", + Version: semver.MustParse("1.0.0"), + Description: "Add contract metadata entries to the Catalog service", + }, + Input: operations.CreateContractMetadataInput{ + ContractMetadata: []cldfdatastore.ContractMetadata{contractMetadata1, contractMetadata2}, + }, + Output: operations.CreateContractMetadataOutput{ + DataStore: testDataStoreWithContractMetadata(t, contractMetadata1, contractMetadata2), + }, + }}, + }, + }, + { + name: "failure: fails to add second entry", + env: cldf.Environment{ + DataStore: testDataStoreWithContractMetadata(t, contractMetadata2).Seal(), + OperationsBundle: cldfoperations.NewBundle(t.Context, cldflogger.Test(t), cldfoperations.NewMemoryReporter()), + }, + input: CreateContractMetadataChangesetInput{ + ContractMetadata: []cldfdatastore.ContractMetadata{contractMetadata1, contractMetadata2}, + }, + wantErr: "failed to create contract metadata entry 1 in catalog store: " + + "a contract metadata record with the supplied key already exists", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + got, err := CreateContractMetadataChangeset{}.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{}))) + } else { + require.ErrorContains(t, err, tt.wantErr) + } + }) + } +} + +// ----- helpers ----- + +func testDataStoreWithContractMetadata( + t *testing.T, metadata ...cldfdatastore.ContractMetadata, +) cldfdatastore.MutableDataStore { + t.Helper() + + ds := cldfdatastore.NewMemoryDataStore() + for _, m := range metadata { + err := ds.ContractMetadata().Add(m) + require.NoError(t, err) + } + + return ds +} diff --git a/catalog/changesets/delete_contract_metadata.go b/catalog/changesets/delete_contract_metadata.go new file mode 100644 index 0000000..de0d8a4 --- /dev/null +++ b/catalog/changesets/delete_contract_metadata.go @@ -0,0 +1,66 @@ +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" +) + +// DeleteContractMetadataChangeset deletes existing contract metadata entries from the Catalog service. +type DeleteContractMetadataChangeset struct{} + +type DeleteContractMetadataChangesetInput struct { + ContractMetadataKeys []cldfdatastore.ContractMetadataKey `json:"contractMetadataKeys"` +} + +// VerifyPreconditions ensures the input is valid. +func (DeleteContractMetadataChangeset) VerifyPreconditions(e cldf.Environment, input DeleteContractMetadataChangesetInput) error { + if len(input.ContractMetadataKeys) == 0 { + return errors.New("missing contract metadata keys input") + } + if e.DataStore == nil { + return errors.New("missing datastore in environment") + } + + uniqKeys := lo.Uniq(input.ContractMetadataKeys) + if len(uniqKeys) != len(input.ContractMetadataKeys) { + return errors.New("duplicate contract metadata keys found in input") + } + + for _, key := range input.ContractMetadataKeys { + _, err := e.DataStore.ContractMetadata().Get(key) + if errors.Is(err, cldfdatastore.ErrContractMetadataNotFound) { + return fmt.Errorf("contract metadata for chain selector %v and address %v does not exist", + key.ChainSelector(), key.Address()) + } + if err != nil { + return fmt.Errorf("failed to retrieve contract metadata for chain selector %v and address %v: %w", + key.ChainSelector(), key.Address(), err) + } + } + + return nil +} + +// Apply executes the changeset, deleting the contract metadata from the Catalog service. +func (DeleteContractMetadataChangeset) Apply(e cldf.Environment, input DeleteContractMetadataChangesetInput) (cldf.ChangesetOutput, error) { + deps := operations.DeleteContractMetadataDeps{DataStore: e.DataStore} + opInput := operations.DeleteContractMetadataInput{ContractMetadataKeys: input.ContractMetadataKeys} + + report, err := cldfops.ExecuteOperation(e.OperationsBundle, operations.DeleteContractMetadataOp, 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/delete_contract_metadata_test.go b/catalog/changesets/delete_contract_metadata_test.go new file mode 100644 index 0000000..8d85356 --- /dev/null +++ b/catalog/changesets/delete_contract_metadata_test.go @@ -0,0 +1,172 @@ +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 TestDeleteContractMetadataChangeset_VerifyPreconditions(t *testing.T) { + t.Parallel() + + key1 := cldfdatastore.NewContractMetadataKey(1234, "0x01") + contractMetadata1 := cldfdatastore.ContractMetadata{Address: "0x01", ChainSelector: 1234, Metadata: "value1"} + + tests := []struct { + name string + env cldf.Environment + input DeleteContractMetadataChangesetInput + wantErr string + }{ + { + name: "success: valid preconditions", + env: cldf.Environment{DataStore: func() cldfdatastore.DataStore { + ds := cldfdatastore.NewMemoryDataStore() + err := ds.ContractMetadata().Add(contractMetadata1) + require.NoError(t, err) + + return ds.Seal() + }()}, + input: DeleteContractMetadataChangesetInput{ + ContractMetadataKeys: []cldfdatastore.ContractMetadataKey{key1}, + }, + }, + { + name: "failure: missing datastore", + env: cldf.Environment{}, + input: DeleteContractMetadataChangesetInput{ + ContractMetadataKeys: []cldfdatastore.ContractMetadataKey{key1}, + }, + wantErr: "missing datastore in environment", + }, + { + name: "failure: no contract metadata keys given", + env: cldf.Environment{DataStore: cldfdatastore.NewMemoryDataStore().Seal()}, + input: DeleteContractMetadataChangesetInput{ + ContractMetadataKeys: []cldfdatastore.ContractMetadataKey{}, + }, + wantErr: "missing contract metadata keys input", + }, + { + name: "failure: duplicate keys", + env: cldf.Environment{DataStore: cldfdatastore.NewMemoryDataStore().Seal()}, + input: DeleteContractMetadataChangesetInput{ + ContractMetadataKeys: []cldfdatastore.ContractMetadataKey{key1, key1}, + }, + wantErr: "duplicate contract metadata keys found in input", + }, + { + name: "failure: contract metadata does not exist", + env: cldf.Environment{DataStore: cldfdatastore.NewMemoryDataStore().Seal()}, + input: DeleteContractMetadataChangesetInput{ + ContractMetadataKeys: []cldfdatastore.ContractMetadataKey{key1}, + }, + wantErr: "contract metadata for chain selector 1234 and address 0x01 does not exist", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + err := DeleteContractMetadataChangeset{}.VerifyPreconditions(tt.env, tt.input) + + if tt.wantErr == "" { + require.NoError(t, err) + } else { + require.ErrorContains(t, err, tt.wantErr) + } + }) + } +} + +func TestDeleteContractMetadataChangeset_Apply(t *testing.T) { + t.Parallel() + + contractMetadata1 := cldfdatastore.ContractMetadata{Address: "0x01", ChainSelector: 1234, Metadata: "value1"} + contractMetadata2 := cldfdatastore.ContractMetadata{Address: "0x02", ChainSelector: 1234, Metadata: "value2"} + + key1 := cldfdatastore.NewContractMetadataKey(1234, "0x01") + key2 := cldfdatastore.NewContractMetadataKey(1234, "0x02") + + tests := []struct { + name string + env cldf.Environment + input DeleteContractMetadataChangesetInput + want cldf.ChangesetOutput + wantErr string + }{ + { + name: "success: deletes two entries from contract metadata", + env: cldf.Environment{ + DataStore: testDataStoreWithContractMetadata(t, contractMetadata1, contractMetadata2).Seal(), + OperationsBundle: cldfoperations.NewBundle(t.Context, cldflogger.Test(t), cldfoperations.NewMemoryReporter()), + }, + input: DeleteContractMetadataChangesetInput{ + ContractMetadataKeys: []cldfdatastore.ContractMetadataKey{key1, key2}, + }, + want: cldf.ChangesetOutput{ + DataStore: testDataStoreWithContractMetadata(t), + Reports: []cldfoperations.Report[any, any]{{ + Def: cldfoperations.Definition{ + ID: "catalog-delete-contract-metadata", + Version: semver.MustParse("1.0.0"), + Description: "Delete contract metadata entries from the Catalog service", + }, + Input: operations.DeleteContractMetadataInput{ + ContractMetadataKeys: []cldfdatastore.ContractMetadataKey{key1, key2}, + }, + Output: operations.DeleteContractMetadataOutput{ + DataStore: testDataStoreWithContractMetadata(t), + }, + }}, + }, + }, + { + name: "failure: fails to delete entry that does not exist", + env: cldf.Environment{ + DataStore: testDataStoreWithContractMetadata(t, contractMetadata1).Seal(), + OperationsBundle: cldfoperations.NewBundle(t.Context, cldflogger.Test(t), cldfoperations.NewMemoryReporter()), + }, + input: DeleteContractMetadataChangesetInput{ + ContractMetadataKeys: []cldfdatastore.ContractMetadataKey{key1, key2}, + }, + wantErr: "failed to delete contract metadata entry 1 from catalog store: " + + "no contract metadata record can be found for the provided key", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + got, err := DeleteContractMetadataChangeset{}.Apply(tt.env, tt.input) + + if tt.wantErr == "" { + require.NoError(t, err) + require.Empty(t, + cmp.Diff(tt.want, got, + contractMetadataKeyComparer, + cmpopts.IgnoreFields(cldfoperations.Report[any, any]{}, "ID", "Timestamp"), + cmpopts.IgnoreUnexported(cldfdatastore.MemoryAddressRefStore{}, cldfdatastore.MemoryChainMetadataStore{}, + cldfdatastore.MemoryContractMetadataStore{}, cldfdatastore.MemoryEnvMetadataStore{}))) + } else { + require.ErrorContains(t, err, tt.wantErr) + } + }) + } +} + +// ----- helpers ----- + +var contractMetadataKeyComparer = cmp.Comparer(func(x, y cldfdatastore.ContractMetadataKey) bool { + return x.Equals(x) +}) diff --git a/catalog/changesets/update_contract_metadata.go b/catalog/changesets/update_contract_metadata.go new file mode 100644 index 0000000..519b5da --- /dev/null +++ b/catalog/changesets/update_contract_metadata.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" +) + +// UpdateContractMetadataChangeset updates existing contract metadata entries in the Catalog service. +type UpdateContractMetadataChangeset struct{} + +type UpdateContractMetadataChangesetInput struct { + ContractMetadata []cldfdatastore.ContractMetadata `json:"contractMetadata"` +} + +// VerifyPreconditions ensures the input is valid. +func (UpdateContractMetadataChangeset) VerifyPreconditions(e cldf.Environment, input UpdateContractMetadataChangesetInput) error { + if len(input.ContractMetadata) == 0 { + return errors.New("missing contract metadata input") + } + if e.DataStore == nil { + return errors.New("missing datastore in environment") + } + + uniqContractMetadata := lo.UniqBy(input.ContractMetadata, func(cm cldfdatastore.ContractMetadata) cldfdatastore.ContractMetadataKey { + return cm.Key() + }) + if len(uniqContractMetadata) != len(input.ContractMetadata) { + return errors.New("duplicate contract metadata entries found in input") + } + + for _, contractMetadata := range input.ContractMetadata { + _, err := e.DataStore.ContractMetadata().Get(contractMetadata.Key()) + if errors.Is(err, cldfdatastore.ErrContractMetadataNotFound) { + return fmt.Errorf("contract metadata for chain selector %v and address %v does not exist", + contractMetadata.ChainSelector, contractMetadata.Address) + } + if err != nil { + return fmt.Errorf("failed to retrieve contract metadata for chain selector %v and address %v: %w", + contractMetadata.ChainSelector, contractMetadata.Address, err) + } + } + + return nil +} + +// Apply executes the changeset, updating the contract metadata in the Catalog service. +func (UpdateContractMetadataChangeset) Apply(e cldf.Environment, input UpdateContractMetadataChangesetInput) (cldf.ChangesetOutput, error) { + deps := operations.UpdateContractMetadataDeps{DataStore: e.DataStore} + opInput := operations.UpdateContractMetadataInput{ContractMetadata: input.ContractMetadata} + + report, err := cldfops.ExecuteOperation(e.OperationsBundle, operations.UpdateContractMetadataOp, 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/update_contract_metadata_test.go b/catalog/changesets/update_contract_metadata_test.go new file mode 100644 index 0000000..0df6d4f --- /dev/null +++ b/catalog/changesets/update_contract_metadata_test.go @@ -0,0 +1,164 @@ +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 TestUpdateContractMetadataChangeset_VerifyPreconditions(t *testing.T) { + t.Parallel() + + contractMetadata1 := cldfdatastore.ContractMetadata{Address: "0x01", ChainSelector: 1234, Metadata: "value1"} + contractMetadata2 := cldfdatastore.ContractMetadata{Address: "0x01", ChainSelector: 1234, Metadata: "value2"} + + tests := []struct { + name string + env cldf.Environment + input UpdateContractMetadataChangesetInput + wantErr string + }{ + { + name: "success: valid preconditions", + env: cldf.Environment{DataStore: func() cldfdatastore.DataStore { + ds := cldfdatastore.NewMemoryDataStore() + err := ds.ContractMetadata().Add(contractMetadata1) + require.NoError(t, err) + + return ds.Seal() + }()}, + input: UpdateContractMetadataChangesetInput{ + ContractMetadata: []cldfdatastore.ContractMetadata{contractMetadata1}, + }, + }, + { + name: "failure: missing datastore", + env: cldf.Environment{}, + input: UpdateContractMetadataChangesetInput{ + ContractMetadata: []cldfdatastore.ContractMetadata{{}}, + }, + wantErr: "missing datastore in environment", + }, + { + name: "failure: no contract metadata given", + env: cldf.Environment{DataStore: cldfdatastore.NewMemoryDataStore().Seal()}, + input: UpdateContractMetadataChangesetInput{ + ContractMetadata: []cldfdatastore.ContractMetadata{}, + }, + wantErr: "missing contract metadata input", + }, + { + name: "failure: duplicate entries", + env: cldf.Environment{DataStore: cldfdatastore.NewMemoryDataStore().Seal()}, + input: UpdateContractMetadataChangesetInput{ + ContractMetadata: []cldfdatastore.ContractMetadata{contractMetadata1, contractMetadata2}, + }, + wantErr: "duplicate contract metadata entries found in input", + }, + { + name: "failure: contract metadata does not exist", + env: cldf.Environment{DataStore: cldfdatastore.NewMemoryDataStore().Seal()}, + input: UpdateContractMetadataChangesetInput{ + ContractMetadata: []cldfdatastore.ContractMetadata{contractMetadata1}, + }, + wantErr: "contract metadata for chain selector 1234 and address 0x01 does not exist", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + err := UpdateContractMetadataChangeset{}.VerifyPreconditions(tt.env, tt.input) + + if tt.wantErr == "" { + require.NoError(t, err) + } else { + require.ErrorContains(t, err, tt.wantErr) + } + }) + } +} + +func TestUpdateContractMetadataChangeset_Apply(t *testing.T) { + t.Parallel() + + contractMetadata1 := cldfdatastore.ContractMetadata{Address: "0x01", ChainSelector: 1234, Metadata: "value1"} + contractMetadata2 := cldfdatastore.ContractMetadata{Address: "0x02", ChainSelector: 1234, Metadata: "value2"} + contractMetadata1Updated := cldfdatastore.ContractMetadata{Address: "0x01", ChainSelector: 1234, Metadata: "updated-value1"} + contractMetadata2Updated := cldfdatastore.ContractMetadata{Address: "0x02", ChainSelector: 1234, Metadata: "updated-value2"} + + tests := []struct { + name string + env cldf.Environment + input UpdateContractMetadataChangesetInput + want cldf.ChangesetOutput + wantErr string + }{ + { + name: "success: updates two entries in contract metadata", + env: cldf.Environment{ + DataStore: testDataStoreWithContractMetadata(t, contractMetadata1, contractMetadata2).Seal(), + OperationsBundle: cldfoperations.NewBundle(t.Context, cldflogger.Test(t), cldfoperations.NewMemoryReporter()), + }, + input: UpdateContractMetadataChangesetInput{ + ContractMetadata: []cldfdatastore.ContractMetadata{contractMetadata1Updated, contractMetadata2Updated}, + }, + want: cldf.ChangesetOutput{ + DataStore: testDataStoreWithContractMetadata(t, contractMetadata1Updated, contractMetadata2Updated), + Reports: []cldfoperations.Report[any, any]{{ + Def: cldfoperations.Definition{ + ID: "catalog-update-contract-metadata", + Version: semver.MustParse("1.0.0"), + Description: "Update contract metadata entries in the Catalog service", + }, + Input: operations.UpdateContractMetadataInput{ + ContractMetadata: []cldfdatastore.ContractMetadata{contractMetadata1Updated, contractMetadata2Updated}, + }, + Output: operations.UpdateContractMetadataOutput{ + DataStore: testDataStoreWithContractMetadata(t, contractMetadata1Updated, contractMetadata2Updated), + }, + }}, + }, + }, + { + name: "failure: fails to update entry that does not exist", + env: cldf.Environment{ + DataStore: testDataStoreWithContractMetadata(t, contractMetadata1).Seal(), + OperationsBundle: cldfoperations.NewBundle(t.Context, cldflogger.Test(t), cldfoperations.NewMemoryReporter()), + }, + input: UpdateContractMetadataChangesetInput{ + ContractMetadata: []cldfdatastore.ContractMetadata{contractMetadata1Updated, contractMetadata2Updated}, + }, + wantErr: "failed to update contract metadata entry 1 in catalog store: " + + "no contract metadata record can be", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + got, err := UpdateContractMetadataChangeset{}.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{}))) + } else { + require.ErrorContains(t, err, tt.wantErr) + } + }) + } +} diff --git a/catalog/operations/create_contract_metadata.go b/catalog/operations/create_contract_metadata.go new file mode 100644 index 0000000..c5f2a16 --- /dev/null +++ b/catalog/operations/create_contract_metadata.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" +) + +// CreateContractMetadataDeps holds non-serializable dependencies for the +// CreateContractMetadataOp operation. +type CreateContractMetadataDeps struct { + DataStore cldfdatastore.DataStore +} + +// CreateContractMetadataInput is the serializable input of a CreateContractMetadataOp invocation. +type CreateContractMetadataInput struct { + ContractMetadata []cldfdatastore.ContractMetadata +} + +// CreateContractMetadataOutput is the serializable output of a CreateContractMetadataOp invocation. +type CreateContractMetadataOutput struct { + DataStore cldfdatastore.MutableDataStore +} + +// CreateContractMetadataOp creates contract metadata entries in the Catalog service. +var CreateContractMetadataOp = cldfops.NewOperation( + "catalog-create-contract-metadata", + semver.MustParse("1.0.0"), + "Add contract metadata entries to the Catalog service", + func(b cldfops.Bundle, deps CreateContractMetadataDeps, input CreateContractMetadataInput) (CreateContractMetadataOutput, error) { + dataStore := cldfdatastore.NewMemoryDataStore() + err := dataStore.Merge(deps.DataStore) + if err != nil { + return CreateContractMetadataOutput{}, fmt.Errorf("failed to create memory data store: %w", err) + } + + for i, item := range input.ContractMetadata { + err = dataStore.ContractMetadata().Add(item) + if err != nil { + return CreateContractMetadataOutput{}, fmt.Errorf("failed to create contract metadata entry %d in catalog store: %w", i, err) + } + } + + b.Logger.Infow("Catalog ContractMetadata created successfully") + + return CreateContractMetadataOutput{DataStore: dataStore}, nil + }, +) diff --git a/catalog/operations/delete_contract_metadata.go b/catalog/operations/delete_contract_metadata.go new file mode 100644 index 0000000..eb60f5c --- /dev/null +++ b/catalog/operations/delete_contract_metadata.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" +) + +// DeleteContractMetadataDeps holds non-serializable dependencies for the +// DeleteContractMetadataOp operation. +type DeleteContractMetadataDeps struct { + DataStore cldfdatastore.DataStore +} + +// DeleteContractMetadataInput is the serializable input of a DeleteContractMetadataOp invocation. +type DeleteContractMetadataInput struct { + ContractMetadataKeys []cldfdatastore.ContractMetadataKey +} + +// DeleteContractMetadataOutput is the serializable output of a DeleteContractMetadataOp invocation. +type DeleteContractMetadataOutput struct { + DataStore cldfdatastore.MutableDataStore +} + +// DeleteContractMetadataOp deletes existing contract metadata entries from the Catalog service. +var DeleteContractMetadataOp = cldfops.NewOperation( + "catalog-delete-contract-metadata", + semver.MustParse("1.0.0"), + "Delete contract metadata entries from the Catalog service", + func(b cldfops.Bundle, deps DeleteContractMetadataDeps, input DeleteContractMetadataInput) (DeleteContractMetadataOutput, error) { + dataStore := cldfdatastore.NewMemoryDataStore() + err := dataStore.Merge(deps.DataStore) + if err != nil { + return DeleteContractMetadataOutput{}, fmt.Errorf("failed to create memory data store: %w", err) + } + + for i, key := range input.ContractMetadataKeys { + err = dataStore.ContractMetadata().Delete(key) + if err != nil { + return DeleteContractMetadataOutput{}, fmt.Errorf("failed to delete contract metadata entry %d from catalog store: %w", i, err) + } + } + + b.Logger.Infow("Catalog ContractMetadata deleted successfully") + + return DeleteContractMetadataOutput{DataStore: dataStore}, nil + }, +) diff --git a/catalog/operations/update_contract_metadata.go b/catalog/operations/update_contract_metadata.go new file mode 100644 index 0000000..dcd0b41 --- /dev/null +++ b/catalog/operations/update_contract_metadata.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" +) + +// UpdateContractMetadataDeps holds non-serializable dependencies for the +// UpdateContractMetadataOp operation. +type UpdateContractMetadataDeps struct { + DataStore cldfdatastore.DataStore +} + +// UpdateContractMetadataInput is the serializable input of a UpdateContractMetadataOp invocation. +type UpdateContractMetadataInput struct { + ContractMetadata []cldfdatastore.ContractMetadata +} + +// UpdateContractMetadataOutput is the serializable output of a UpdateContractMetadataOp invocation. +type UpdateContractMetadataOutput struct { + DataStore cldfdatastore.MutableDataStore +} + +// UpdateContractMetadataOp updates existing contract metadata entries in the Catalog service. +var UpdateContractMetadataOp = cldfops.NewOperation( + "catalog-update-contract-metadata", + semver.MustParse("1.0.0"), + "Update contract metadata entries in the Catalog service", + func(b cldfops.Bundle, deps UpdateContractMetadataDeps, input UpdateContractMetadataInput) (UpdateContractMetadataOutput, error) { + dataStore := cldfdatastore.NewMemoryDataStore() + err := dataStore.Merge(deps.DataStore) + if err != nil { + return UpdateContractMetadataOutput{}, fmt.Errorf("failed to create memory data store: %w", err) + } + + for i, item := range input.ContractMetadata { + err = dataStore.ContractMetadata().Update(item) + if err != nil { + return UpdateContractMetadataOutput{}, fmt.Errorf("failed to update contract metadata entry %d in catalog store: %w", i, err) + } + } + + b.Logger.Infow("Catalog ContractMetadata updated successfully") + + return UpdateContractMetadataOutput{DataStore: dataStore}, nil + }, +) diff --git a/go.mod b/go.mod index 96ef8fd..c9f77e5 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,8 @@ require ( github.com/deckarep/golang-set/v2 v2.6.0 github.com/ethereum/go-ethereum v1.17.1 github.com/gagliardetto/solana-go v1.13.0 + github.com/google/go-cmp v0.7.0 + github.com/samber/lo v1.52.0 github.com/smartcontractkit/ccip-owner-contracts v0.1.0 github.com/smartcontractkit/chain-selectors v1.0.97 github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260121163256-85accaf3d28d @@ -114,7 +116,6 @@ require ( github.com/golang/protobuf v1.5.4 // indirect github.com/golang/snappy v1.0.0 // indirect github.com/google/gnostic-models v0.6.9 // indirect - github.com/google/go-cmp v0.7.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/websocket v1.5.3 // indirect @@ -204,7 +205,6 @@ require ( github.com/rs/zerolog v1.34.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sagikazarmark/locafero v0.11.0 // indirect - github.com/samber/lo v1.52.0 // indirect github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 // indirect github.com/scylladb/go-reflectx v1.0.1 // indirect github.com/segmentio/ksuid v1.0.4 // indirect