Skip to content

Commit 6ec4262

Browse files
feat: add catalog create contract metadata changeset
1 parent 6d9010c commit 6ec4262

4 files changed

Lines changed: 299 additions & 2 deletions

File tree

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
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+
// CreateContractMetadataChangeset creates contract metadata entries in the Catalog service.
16+
type CreateContractMetadataChangeset struct{}
17+
18+
type CreateContractMetadataChangesetInput struct {
19+
ContractMetadata []cldfdatastore.ContractMetadata `json:"contractMetadata"`
20+
}
21+
22+
// VerifyPreconditions ensures the input is valid.
23+
func (CreateContractMetadataChangeset) VerifyPreconditions(e cldf.Environment, input CreateContractMetadataChangesetInput) 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+
uniqKeys := lo.UniqBy(input.ContractMetadata, func(cm cldfdatastore.ContractMetadata) cldfdatastore.ContractMetadataKey {
32+
return cm.Key()
33+
})
34+
if len(uniqKeys) != 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(cldfdatastore.NewContractMetadataKey(contractMetadata.ChainSelector, contractMetadata.Address))
40+
if err == nil {
41+
return fmt.Errorf("contract metadata for chain selector %v and address %v already exists",
42+
contractMetadata.ChainSelector, contractMetadata.Address)
43+
}
44+
if !errors.Is(err, cldfdatastore.ErrContractMetadataNotFound) {
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, adding the contract metadata to the Catalog service.
54+
func (CreateContractMetadataChangeset) Apply(e cldf.Environment, input CreateContractMetadataChangesetInput) (cldf.ChangesetOutput, error) {
55+
deps := operations.CreateContractMetadataDeps{DataStore: e.DataStore}
56+
opInput := operations.CreateContractMetadataInput{ContractMetadata: input.ContractMetadata}
57+
58+
report, err := cldfops.ExecuteOperation(e.OperationsBundle, operations.CreateContractMetadataOp, deps, opInput)
59+
out := cldf.ChangesetOutput{
60+
DataStore: report.Output.DataStore,
61+
Reports: []cldfops.Report[any, any]{report.ToGenericReport()},
62+
}
63+
if err != nil {
64+
return out, err
65+
}
66+
67+
return out, nil
68+
}
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
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 TestCreateContractMetadataChangeset_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 CreateContractMetadataChangesetInput
29+
wantErr string
30+
}{
31+
{
32+
name: "success: valid preconditions",
33+
env: cldf.Environment{DataStore: cldfdatastore.NewMemoryDataStore().Seal()},
34+
input: CreateContractMetadataChangesetInput{
35+
ContractMetadata: []cldfdatastore.ContractMetadata{contractMetadata1},
36+
},
37+
},
38+
{
39+
name: "failure: missing datastore",
40+
env: cldf.Environment{},
41+
input: CreateContractMetadataChangesetInput{
42+
ContractMetadata: []cldfdatastore.ContractMetadata{{}},
43+
},
44+
wantErr: "missing datastore in environment",
45+
},
46+
{
47+
name: "failure: no contract metadata given",
48+
env: cldf.Environment{DataStore: cldfdatastore.NewMemoryDataStore().Seal()},
49+
input: CreateContractMetadataChangesetInput{
50+
ContractMetadata: []cldfdatastore.ContractMetadata{},
51+
},
52+
wantErr: "missing contract metadata input",
53+
},
54+
{
55+
name: "failure: duplicate entries",
56+
env: cldf.Environment{DataStore: cldfdatastore.NewMemoryDataStore().Seal()},
57+
input: CreateContractMetadataChangesetInput{
58+
ContractMetadata: []cldfdatastore.ContractMetadata{contractMetadata1, contractMetadata2},
59+
},
60+
wantErr: "duplicate contract metadata entries found in input",
61+
},
62+
{
63+
name: "failure: contract metadata already exists",
64+
env: cldf.Environment{DataStore: func() cldfdatastore.DataStore {
65+
ds := cldfdatastore.NewMemoryDataStore()
66+
err := ds.ContractMetadata().Add(contractMetadata1)
67+
require.NoError(t, err)
68+
69+
return ds.Seal()
70+
}()},
71+
input: CreateContractMetadataChangesetInput{
72+
ContractMetadata: []cldfdatastore.ContractMetadata{contractMetadata1},
73+
},
74+
wantErr: "contract metadata for chain selector 1234 and address 0x01 already exists",
75+
},
76+
}
77+
for _, tt := range tests {
78+
t.Run(tt.name, func(t *testing.T) {
79+
t.Parallel()
80+
81+
err := CreateContractMetadataChangeset{}.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 TestCreateContractMetadataChangeset_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+
98+
tests := []struct {
99+
name string
100+
env cldf.Environment
101+
input CreateContractMetadataChangesetInput
102+
want cldf.ChangesetOutput
103+
wantErr string
104+
}{
105+
{
106+
name: "success: adds two entries to contract metadata",
107+
env: cldf.Environment{
108+
DataStore: testDataStoreWithContractMetadata(t).Seal(),
109+
OperationsBundle: cldfoperations.NewBundle(t.Context, cldflogger.Test(t), cldfoperations.NewMemoryReporter()),
110+
},
111+
input: CreateContractMetadataChangesetInput{
112+
ContractMetadata: []cldfdatastore.ContractMetadata{contractMetadata1, contractMetadata2},
113+
},
114+
want: cldf.ChangesetOutput{
115+
DataStore: testDataStoreWithContractMetadata(t, contractMetadata1, contractMetadata2),
116+
Reports: []cldfoperations.Report[any, any]{{
117+
Def: cldfoperations.Definition{
118+
ID: "catalog-create-contract-metadata",
119+
Version: semver.MustParse("1.0.0"),
120+
Description: "Add contract metadata entries to the Catalog service",
121+
},
122+
Input: operations.CreateContractMetadataInput{
123+
ContractMetadata: []cldfdatastore.ContractMetadata{contractMetadata1, contractMetadata2},
124+
},
125+
Output: operations.CreateContractMetadataOutput{
126+
DataStore: testDataStoreWithContractMetadata(t, contractMetadata1, contractMetadata2),
127+
},
128+
}},
129+
},
130+
},
131+
{
132+
name: "failure: fails to add second entry",
133+
env: cldf.Environment{
134+
DataStore: testDataStoreWithContractMetadata(t, contractMetadata2).Seal(),
135+
OperationsBundle: cldfoperations.NewBundle(t.Context, cldflogger.Test(t), cldfoperations.NewMemoryReporter()),
136+
},
137+
input: CreateContractMetadataChangesetInput{
138+
ContractMetadata: []cldfdatastore.ContractMetadata{contractMetadata1, contractMetadata2},
139+
},
140+
wantErr: "failed to create contract metadata entry 1 in catalog store: " +
141+
"a contract metadata record with the supplied key already exists",
142+
},
143+
}
144+
for _, tt := range tests {
145+
t.Run(tt.name, func(t *testing.T) {
146+
t.Parallel()
147+
148+
got, err := CreateContractMetadataChangeset{}.Apply(tt.env, tt.input)
149+
150+
if tt.wantErr == "" {
151+
require.NoError(t, err)
152+
require.Empty(t,
153+
cmp.Diff(tt.want, got,
154+
cmpopts.IgnoreFields(cldfoperations.Report[any, any]{}, "ID", "Timestamp"),
155+
cmpopts.IgnoreUnexported(cldfdatastore.MemoryAddressRefStore{}, cldfdatastore.MemoryChainMetadataStore{},
156+
cldfdatastore.MemoryContractMetadataStore{}, cldfdatastore.MemoryEnvMetadataStore{})))
157+
} else {
158+
require.ErrorContains(t, err, tt.wantErr)
159+
}
160+
})
161+
}
162+
}
163+
164+
// ----- helpers -----
165+
166+
func testDataStoreWithContractMetadata(
167+
t *testing.T, metadata ...cldfdatastore.ContractMetadata,
168+
) cldfdatastore.MutableDataStore {
169+
t.Helper()
170+
171+
ds := cldfdatastore.NewMemoryDataStore()
172+
for _, m := range metadata {
173+
err := ds.ContractMetadata().Add(m)
174+
require.NoError(t, err)
175+
}
176+
177+
return ds
178+
}
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+
// CreateContractMetadataDeps holds non-serializable dependencies for the
13+
// CatalogCreateContractMetadataOp operation.
14+
type CreateContractMetadataDeps struct {
15+
DataStore cldfdatastore.DataStore
16+
}
17+
18+
// CreateContractMetadataInput is the serializable input of a CatalogCreateContractMetadataOp invocation.
19+
type CreateContractMetadataInput struct {
20+
ContractMetadata []cldfdatastore.ContractMetadata
21+
}
22+
23+
// CreateContractMetadataOutput is the serializable output of a CatalogCreateContractMetadataOp invocation.
24+
type CreateContractMetadataOutput struct {
25+
DataStore cldfdatastore.MutableDataStore
26+
}
27+
28+
// CreateContractMetadataOp creates contract metadata entries in the Catalog service.
29+
var CreateContractMetadataOp = cldfops.NewOperation(
30+
"catalog-create-contract-metadata",
31+
semver.MustParse("1.0.0"),
32+
"Add contract metadata entries to the Catalog service",
33+
func(b cldfops.Bundle, deps CreateContractMetadataDeps, input CreateContractMetadataInput) (CreateContractMetadataOutput, error) {
34+
dataStore := cldfdatastore.NewMemoryDataStore()
35+
err := dataStore.Merge(deps.DataStore)
36+
if err != nil {
37+
return CreateContractMetadataOutput{}, fmt.Errorf("failed to create memory data store: %w", err)
38+
}
39+
40+
for i, item := range input.ContractMetadata {
41+
err = dataStore.ContractMetadata().Add(item)
42+
if err != nil {
43+
return CreateContractMetadataOutput{}, fmt.Errorf("failed to create contract metadata entry %d in catalog store: %w", i, err)
44+
}
45+
}
46+
47+
b.Logger.Infow("Catalog ContractMetadata created successfully")
48+
49+
return CreateContractMetadataOutput{DataStore: dataStore}, nil
50+
},
51+
)

go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ require (
88
github.com/deckarep/golang-set/v2 v2.6.0
99
github.com/ethereum/go-ethereum v1.17.1
1010
github.com/gagliardetto/solana-go v1.13.0
11+
github.com/google/go-cmp v0.7.0
12+
github.com/samber/lo v1.52.0
1113
github.com/smartcontractkit/ccip-owner-contracts v0.1.0
1214
github.com/smartcontractkit/chain-selectors v1.0.97
1315
github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260121163256-85accaf3d28d
@@ -114,7 +116,6 @@ require (
114116
github.com/golang/protobuf v1.5.4 // indirect
115117
github.com/golang/snappy v1.0.0 // indirect
116118
github.com/google/gnostic-models v0.6.9 // indirect
117-
github.com/google/go-cmp v0.7.0 // indirect
118119
github.com/google/gofuzz v1.2.0 // indirect
119120
github.com/google/uuid v1.6.0 // indirect
120121
github.com/gorilla/websocket v1.5.3 // indirect
@@ -204,7 +205,6 @@ require (
204205
github.com/rs/zerolog v1.34.0 // indirect
205206
github.com/russross/blackfriday/v2 v2.1.0 // indirect
206207
github.com/sagikazarmark/locafero v0.11.0 // indirect
207-
github.com/samber/lo v1.52.0 // indirect
208208
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 // indirect
209209
github.com/scylladb/go-reflectx v1.0.1 // indirect
210210
github.com/segmentio/ksuid v1.0.4 // indirect

0 commit comments

Comments
 (0)