Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions catalog/changesets/create_contract_metadata.go
Original file line number Diff line number Diff line change
@@ -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
}
178 changes: 178 additions & 0 deletions catalog/changesets/create_contract_metadata_test.go
Original file line number Diff line number Diff line change
@@ -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
}
66 changes: 66 additions & 0 deletions catalog/changesets/delete_contract_metadata.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading