Skip to content

Commit 1687d14

Browse files
feat: add catalog update contract metadata changeset (#29)
* feat: add catalog update contract metadata changeset * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
1 parent 9ec54b5 commit 1687d14

3 files changed

Lines changed: 285 additions & 0 deletions

File tree

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package changesets
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
7+
"github.com/samber/lo"
8+
cldfdatastore "github.com/smartcontractkit/chainlink-deployments-framework/datastore"
9+
cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment"
10+
cldfops "github.com/smartcontractkit/chainlink-deployments-framework/operations"
11+
12+
"github.com/smartcontractkit/cld-changesets/catalog/operations"
13+
)
14+
15+
// UpdateContractMetadataChangeset updates existing contract metadata entries in the Catalog service.
16+
type UpdateContractMetadataChangeset struct{}
17+
18+
type UpdateContractMetadataChangesetInput struct {
19+
ContractMetadata []cldfdatastore.ContractMetadata `json:"contractMetadata"`
20+
}
21+
22+
// VerifyPreconditions ensures the input is valid.
23+
func (UpdateContractMetadataChangeset) VerifyPreconditions(e cldf.Environment, input UpdateContractMetadataChangesetInput) error {
24+
if len(input.ContractMetadata) == 0 {
25+
return errors.New("missing contract metadata input")
26+
}
27+
if e.DataStore == nil {
28+
return errors.New("missing datastore in environment")
29+
}
30+
31+
uniqContractMetadata := lo.UniqBy(input.ContractMetadata, func(cm cldfdatastore.ContractMetadata) cldfdatastore.ContractMetadataKey {
32+
return cm.Key()
33+
})
34+
if len(uniqContractMetadata) != len(input.ContractMetadata) {
35+
return errors.New("duplicate contract metadata entries found in input")
36+
}
37+
38+
for _, contractMetadata := range input.ContractMetadata {
39+
_, err := e.DataStore.ContractMetadata().Get(contractMetadata.Key())
40+
if errors.Is(err, cldfdatastore.ErrContractMetadataNotFound) {
41+
return fmt.Errorf("contract metadata for chain selector %v and address %v does not exist",
42+
contractMetadata.ChainSelector, contractMetadata.Address)
43+
}
44+
if err != nil {
45+
return fmt.Errorf("failed to retrieve contract metadata for chain selector %v and address %v: %w",
46+
contractMetadata.ChainSelector, contractMetadata.Address, err)
47+
}
48+
}
49+
50+
return nil
51+
}
52+
53+
// Apply executes the changeset, updating the contract metadata in the Catalog service.
54+
func (UpdateContractMetadataChangeset) Apply(
55+
e cldf.Environment, input UpdateContractMetadataChangesetInput,
56+
) (cldf.ChangesetOutput, error) {
57+
deps := operations.UpdateContractMetadataDeps{DataStore: e.DataStore}
58+
opInput := operations.UpdateContractMetadataInput{ContractMetadata: input.ContractMetadata}
59+
60+
report, err := cldfops.ExecuteOperation(e.OperationsBundle, operations.UpdateContractMetadataOp, deps, opInput)
61+
out := cldf.ChangesetOutput{
62+
DataStore: report.Output.DataStore,
63+
Reports: []cldfops.Report[any, any]{report.ToGenericReport()},
64+
}
65+
if err != nil {
66+
return out, err
67+
}
68+
69+
return out, nil
70+
}
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
package changesets
2+
3+
import (
4+
"testing"
5+
6+
"github.com/Masterminds/semver/v3"
7+
"github.com/google/go-cmp/cmp"
8+
"github.com/google/go-cmp/cmp/cmpopts"
9+
"github.com/stretchr/testify/require"
10+
11+
cldfdatastore "github.com/smartcontractkit/chainlink-deployments-framework/datastore"
12+
cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment"
13+
cldfoperations "github.com/smartcontractkit/chainlink-deployments-framework/operations"
14+
cldflogger "github.com/smartcontractkit/chainlink-deployments-framework/pkg/logger"
15+
16+
"github.com/smartcontractkit/cld-changesets/catalog/operations"
17+
)
18+
19+
func TestUpdateContractMetadataChangeset_VerifyPreconditions(t *testing.T) {
20+
t.Parallel()
21+
22+
contractMetadata1 := cldfdatastore.ContractMetadata{Address: "0x01", ChainSelector: 1234, Metadata: "value1"}
23+
contractMetadata2 := cldfdatastore.ContractMetadata{Address: "0x01", ChainSelector: 1234, Metadata: "value2"}
24+
25+
tests := []struct {
26+
name string
27+
env cldf.Environment
28+
input UpdateContractMetadataChangesetInput
29+
wantErr string
30+
}{
31+
{
32+
name: "success: valid preconditions",
33+
env: cldf.Environment{DataStore: func() cldfdatastore.DataStore {
34+
ds := cldfdatastore.NewMemoryDataStore()
35+
err := ds.ContractMetadata().Add(contractMetadata1)
36+
require.NoError(t, err)
37+
38+
return ds.Seal()
39+
}()},
40+
input: UpdateContractMetadataChangesetInput{
41+
ContractMetadata: []cldfdatastore.ContractMetadata{contractMetadata1},
42+
},
43+
},
44+
{
45+
name: "failure: missing datastore",
46+
env: cldf.Environment{},
47+
input: UpdateContractMetadataChangesetInput{
48+
ContractMetadata: []cldfdatastore.ContractMetadata{{}},
49+
},
50+
wantErr: "missing datastore in environment",
51+
},
52+
{
53+
name: "failure: no contract metadata given",
54+
env: cldf.Environment{DataStore: cldfdatastore.NewMemoryDataStore().Seal()},
55+
input: UpdateContractMetadataChangesetInput{
56+
ContractMetadata: []cldfdatastore.ContractMetadata{},
57+
},
58+
wantErr: "missing contract metadata input",
59+
},
60+
{
61+
name: "failure: duplicate entries",
62+
env: cldf.Environment{DataStore: cldfdatastore.NewMemoryDataStore().Seal()},
63+
input: UpdateContractMetadataChangesetInput{
64+
ContractMetadata: []cldfdatastore.ContractMetadata{contractMetadata1, contractMetadata2},
65+
},
66+
wantErr: "duplicate contract metadata entries found in input",
67+
},
68+
{
69+
name: "failure: contract metadata does not exist",
70+
env: cldf.Environment{DataStore: cldfdatastore.NewMemoryDataStore().Seal()},
71+
input: UpdateContractMetadataChangesetInput{
72+
ContractMetadata: []cldfdatastore.ContractMetadata{contractMetadata1},
73+
},
74+
wantErr: "contract metadata for chain selector 1234 and address 0x01 does not exist",
75+
},
76+
}
77+
for _, tt := range tests {
78+
t.Run(tt.name, func(t *testing.T) {
79+
t.Parallel()
80+
81+
err := UpdateContractMetadataChangeset{}.VerifyPreconditions(tt.env, tt.input)
82+
83+
if tt.wantErr == "" {
84+
require.NoError(t, err)
85+
} else {
86+
require.ErrorContains(t, err, tt.wantErr)
87+
}
88+
})
89+
}
90+
}
91+
92+
func TestUpdateContractMetadataChangeset_Apply(t *testing.T) {
93+
t.Parallel()
94+
95+
contractMetadata1 := cldfdatastore.ContractMetadata{Address: "0x01", ChainSelector: 1234, Metadata: "value1"}
96+
contractMetadata2 := cldfdatastore.ContractMetadata{Address: "0x02", ChainSelector: 1234, Metadata: "value2"}
97+
contractMetadata1Updated := cldfdatastore.ContractMetadata{Address: "0x01", ChainSelector: 1234, Metadata: "updated-value1"}
98+
contractMetadata2Updated := cldfdatastore.ContractMetadata{Address: "0x02", ChainSelector: 1234, Metadata: "updated-value2"}
99+
100+
tests := []struct {
101+
name string
102+
env cldf.Environment
103+
input UpdateContractMetadataChangesetInput
104+
want cldf.ChangesetOutput
105+
wantErr string
106+
}{
107+
{
108+
name: "success: updates two entries in contract metadata",
109+
env: cldf.Environment{
110+
DataStore: testDataStoreWithContractMetadata(t, contractMetadata1, contractMetadata2).Seal(),
111+
OperationsBundle: cldfoperations.NewBundle(t.Context, cldflogger.Test(t), cldfoperations.NewMemoryReporter()),
112+
},
113+
input: UpdateContractMetadataChangesetInput{
114+
ContractMetadata: []cldfdatastore.ContractMetadata{contractMetadata1Updated, contractMetadata2Updated},
115+
},
116+
want: cldf.ChangesetOutput{
117+
DataStore: testDataStoreWithContractMetadata(t, contractMetadata1Updated, contractMetadata2Updated),
118+
Reports: []cldfoperations.Report[any, any]{{
119+
Def: cldfoperations.Definition{
120+
ID: "catalog-update-contract-metadata",
121+
Version: semver.MustParse("1.0.0"),
122+
Description: "Update contract metadata entries in the Catalog service",
123+
},
124+
Input: operations.UpdateContractMetadataInput{
125+
ContractMetadata: []cldfdatastore.ContractMetadata{contractMetadata1Updated, contractMetadata2Updated},
126+
},
127+
Output: operations.UpdateContractMetadataOutput{
128+
DataStore: testDataStoreWithContractMetadata(t, contractMetadata1Updated, contractMetadata2Updated),
129+
},
130+
}},
131+
},
132+
},
133+
{
134+
name: "failure: fails to update entry that does not exist",
135+
env: cldf.Environment{
136+
DataStore: testDataStoreWithContractMetadata(t, contractMetadata1).Seal(),
137+
OperationsBundle: cldfoperations.NewBundle(t.Context, cldflogger.Test(t), cldfoperations.NewMemoryReporter()),
138+
},
139+
input: UpdateContractMetadataChangesetInput{
140+
ContractMetadata: []cldfdatastore.ContractMetadata{contractMetadata1Updated, contractMetadata2Updated},
141+
},
142+
wantErr: "failed to update contract metadata entry 1 in catalog store: " +
143+
"no contract metadata record can be found for the provided key",
144+
},
145+
}
146+
for _, tt := range tests {
147+
t.Run(tt.name, func(t *testing.T) {
148+
t.Parallel()
149+
150+
got, err := UpdateContractMetadataChangeset{}.Apply(tt.env, tt.input)
151+
152+
if tt.wantErr == "" {
153+
require.NoError(t, err)
154+
require.Empty(t,
155+
cmp.Diff(tt.want, got,
156+
cmpopts.IgnoreFields(cldfoperations.Report[any, any]{}, "ID", "Timestamp"),
157+
cmpopts.IgnoreUnexported(cldfdatastore.MemoryAddressRefStore{}, cldfdatastore.MemoryChainMetadataStore{},
158+
cldfdatastore.MemoryContractMetadataStore{}, cldfdatastore.MemoryEnvMetadataStore{})))
159+
} else {
160+
require.ErrorContains(t, err, tt.wantErr)
161+
}
162+
})
163+
}
164+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package operations
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/Masterminds/semver/v3"
7+
8+
cldfdatastore "github.com/smartcontractkit/chainlink-deployments-framework/datastore"
9+
cldfops "github.com/smartcontractkit/chainlink-deployments-framework/operations"
10+
)
11+
12+
// UpdateContractMetadataDeps holds non-serializable dependencies for the
13+
// UpdateContractMetadataOp operation.
14+
type UpdateContractMetadataDeps struct {
15+
DataStore cldfdatastore.DataStore
16+
}
17+
18+
// UpdateContractMetadataInput is the serializable input of an UpdateContractMetadataOp invocation.
19+
type UpdateContractMetadataInput struct {
20+
ContractMetadata []cldfdatastore.ContractMetadata
21+
}
22+
23+
// UpdateContractMetadataOutput is the serializable output of an UpdateContractMetadataOp invocation.
24+
type UpdateContractMetadataOutput struct {
25+
DataStore cldfdatastore.MutableDataStore
26+
}
27+
28+
// UpdateContractMetadataOp updates existing contract metadata entries in the Catalog service.
29+
var UpdateContractMetadataOp = cldfops.NewOperation(
30+
"catalog-update-contract-metadata",
31+
semver.MustParse("1.0.0"),
32+
"Update contract metadata entries in the Catalog service",
33+
func(b cldfops.Bundle, deps UpdateContractMetadataDeps, input UpdateContractMetadataInput) (UpdateContractMetadataOutput, error) {
34+
dataStore := cldfdatastore.NewMemoryDataStore()
35+
err := dataStore.Merge(deps.DataStore)
36+
if err != nil {
37+
return UpdateContractMetadataOutput{}, fmt.Errorf("failed to create memory data store: %w", err)
38+
}
39+
40+
for i, item := range input.ContractMetadata {
41+
err = dataStore.ContractMetadata().Update(item)
42+
if err != nil {
43+
return UpdateContractMetadataOutput{}, fmt.Errorf("failed to update contract metadata entry %d in catalog store: %w", i, err)
44+
}
45+
}
46+
47+
b.Logger.Infow("Catalog ContractMetadata updated successfully")
48+
49+
return UpdateContractMetadataOutput{DataStore: dataStore}, nil
50+
},
51+
)

0 commit comments

Comments
 (0)