Skip to content

Commit bf62b62

Browse files
authored
feat(datastore): adds ChainMetadata (#177)
This PR adds a new metadata type for storing information related to a chain. Similar to other sections of the datastore that support custom domain metadata, chain metadata uses the chain selector as the key and includes a metadata field of type any.
1 parent b4b05b4 commit bf62b62

11 files changed

+1052
-16
lines changed

.changeset/lemon-cars-move.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"chainlink-deployments-framework": patch
3+
---
4+
5+
feat(datastore): adds ChainMetadata

datastore/chain_metadata.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package datastore
2+
3+
import "errors"
4+
5+
var ErrChainMetadataNotFound = errors.New("no chain metadata record can be found for the provided key")
6+
var ErrChainMetadataExists = errors.New("a chain metadata record with the supplied key already exists")
7+
8+
// ChainMetadata implements the UniqueRecord interface
9+
var _ UniqueRecord[ChainMetadataKey, ChainMetadata] = ChainMetadata{}
10+
11+
// ChainMetadata is a struct that holds the metadata for a specific chain.
12+
// It implements the UniqueRecord interface and is used to store chain metadata in the datastore.
13+
// NOTE: Metadata can be of any type. To convert from any to a specific type, use the utility method As.
14+
type ChainMetadata struct {
15+
// ChainSelector refers to the chain associated with the metadata.
16+
ChainSelector uint64 `json:"chainSelector"`
17+
// Metadata is the metadata associated with the chain.
18+
Metadata any `json:"metadata"`
19+
}
20+
21+
// Clone creates a copy of the ChainMetadata.
22+
func (r ChainMetadata) Clone() (ChainMetadata, error) {
23+
metaClone, err := clone(r.Metadata)
24+
if err != nil {
25+
return ChainMetadata{}, err
26+
}
27+
28+
return ChainMetadata{
29+
ChainSelector: r.ChainSelector,
30+
Metadata: metaClone,
31+
}, nil
32+
}
33+
34+
// Key returns the ChainMetadataKey for the ChainMetadata.
35+
// It is used to uniquely identify the chain metadata in the datastore.
36+
func (r ChainMetadata) Key() ChainMetadataKey {
37+
return NewChainMetadataKey(r.ChainSelector)
38+
}

datastore/chain_metadata_key.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package datastore
2+
3+
import (
4+
"fmt"
5+
"strconv"
6+
)
7+
8+
// ChainMetadataKey is an interface that represents a key for ChainMetadata records.
9+
// It is used to uniquely identify a record in the ChainMetadataStore.
10+
type ChainMetadataKey interface {
11+
Comparable[ChainMetadataKey]
12+
fmt.Stringer
13+
14+
// ChainSelector returns the chain-selector of the chain associated with the metadata.
15+
ChainSelector() uint64
16+
}
17+
18+
// contractMetadataKey implements the ChainMetadataKey interface.
19+
var _ ChainMetadataKey = chainMetadataKey{}
20+
21+
// chainMetadataKey is a struct that implements the ChainMetadataKey interface.
22+
// It is used to uniquely identify a record in the ChainMetadataStore.
23+
type chainMetadataKey struct {
24+
chainSelector uint64
25+
}
26+
27+
// ChainSelector returns the chain-selector of the chain associated with the metadata.
28+
func (c chainMetadataKey) ChainSelector() uint64 { return c.chainSelector }
29+
30+
// Equals returns true if the two ChainMetadataKey instances are equal, false otherwise.
31+
func (c chainMetadataKey) Equals(other ChainMetadataKey) bool {
32+
return c.chainSelector == other.ChainSelector()
33+
}
34+
35+
// String returns a string representation of the ChainMetadataKey.
36+
func (c chainMetadataKey) String() string {
37+
return strconv.FormatUint(c.chainSelector, 10)
38+
}
39+
40+
// NewChainMetadataKey creates a new ChainMetadataKey instance.
41+
func NewChainMetadataKey(chainSelector uint64) ChainMetadataKey {
42+
return chainMetadataKey{
43+
chainSelector: chainSelector,
44+
}
45+
}

datastore/chain_metadata_key_test.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package datastore
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/require"
7+
)
8+
9+
func TestChainMetadataKey_Equals(t *testing.T) {
10+
t.Parallel()
11+
12+
tests := []struct {
13+
name string
14+
key1 ChainMetadataKey
15+
key2 ChainMetadataKey
16+
expected bool
17+
}{
18+
{
19+
name: "Equal keys",
20+
key1: NewChainMetadataKey(1),
21+
key2: NewChainMetadataKey(1),
22+
expected: true,
23+
},
24+
{
25+
name: "Different keys",
26+
key1: NewChainMetadataKey(1),
27+
key2: NewChainMetadataKey(2),
28+
expected: false,
29+
},
30+
}
31+
32+
for _, tt := range tests {
33+
t.Run(tt.name, func(t *testing.T) {
34+
t.Parallel()
35+
36+
require.Equal(t, tt.expected, tt.key1.Equals(tt.key2))
37+
})
38+
}
39+
}
40+
41+
func TestChainMetadataKey_ChainSelector(t *testing.T) {
42+
t.Parallel()
43+
44+
chainSelector := uint64(1)
45+
46+
key := NewChainMetadataKey(chainSelector)
47+
48+
require.Equal(t, chainSelector, key.ChainSelector(), "ChainSelector should return the correct chain selector")
49+
}
50+
51+
func TestChainMetadataKey_String(t *testing.T) {
52+
t.Parallel()
53+
54+
key := NewChainMetadataKey(99)
55+
expected := "99"
56+
require.Equal(t, expected, key.String())
57+
}

datastore/chain_metadata_test.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package datastore
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/require"
7+
8+
chain_selectors "github.com/smartcontractkit/chain-selectors"
9+
)
10+
11+
func TestChainMetadata_Clone(t *testing.T) {
12+
t.Parallel()
13+
14+
original := ChainMetadata{
15+
ChainSelector: 1,
16+
Metadata: testMetadata{
17+
Field: "test field",
18+
ChainSelector: chain_selectors.APTOS_MAINNET.Selector,
19+
},
20+
}
21+
22+
cloned, err := original.Clone()
23+
require.NoError(t, err, "Clone should not return an error")
24+
25+
require.Equal(t, original.ChainSelector, cloned.ChainSelector)
26+
27+
concrete, err := As[testMetadata](cloned.Metadata)
28+
require.NoError(t, err, "As should not return an error for CustomMetadata")
29+
require.Equal(t, original.Metadata, concrete)
30+
31+
// Modify the original and ensure the cloned remains unchanged
32+
original.ChainSelector = 2
33+
original.Metadata = testMetadata{
34+
Field: "updated field",
35+
ChainSelector: chain_selectors.APTOS_MAINNET.Selector,
36+
}
37+
38+
require.NotEqual(t, original.ChainSelector, cloned.ChainSelector)
39+
40+
concrete, err = As[testMetadata](cloned.Metadata)
41+
require.NoError(t, err, "As should not return an error for CustomMetadata")
42+
require.NotEqual(t, original.Metadata, concrete, "Cloned metadata should not be equal to modified original")
43+
}
44+
45+
func TestChainMetadata_Key(t *testing.T) {
46+
t.Parallel()
47+
48+
metadata := ChainMetadata{
49+
ChainSelector: 1,
50+
Metadata: testMetadata{Field: "test data", ChainSelector: 0},
51+
}
52+
53+
key := metadata.Key()
54+
expectedKey := NewChainMetadataKey(1)
55+
56+
require.Equal(t, expectedKey, key)
57+
}

datastore/filters.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,3 +87,17 @@ func ContractMetadataByChainSelector(chainSelector uint64) FilterFunc[ContractMe
8787
return filtered
8888
}
8989
}
90+
91+
// ChainMetadataByChainSelector returns a filter that only includes records with the provided chain selector.
92+
func ChainMetadataByChainSelector(chainSelector uint64) FilterFunc[ChainMetadataKey, ChainMetadata] {
93+
return func(records []ChainMetadata) []ChainMetadata {
94+
filtered := make([]ChainMetadata, 0, len(records)) // Pre-allocate capacity
95+
for _, record := range records {
96+
if record.ChainSelector == chainSelector {
97+
filtered = append(filtered, record)
98+
}
99+
}
100+
101+
return filtered
102+
}
103+
}

datastore/filters_test.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -406,3 +406,63 @@ func TestContractMetadataByChainSelector(t *testing.T) {
406406
})
407407
}
408408
}
409+
410+
func TestChainMetadataByChainSelector(t *testing.T) {
411+
t.Parallel()
412+
413+
var (
414+
recordOne = ChainMetadata{
415+
ChainSelector: 1,
416+
Metadata: testMetadata{Field: "Record1", ChainSelector: 0},
417+
}
418+
recordTwo = ChainMetadata{
419+
ChainSelector: 2,
420+
Metadata: testMetadata{Field: "Record2", ChainSelector: 0},
421+
}
422+
recordThree = ChainMetadata{
423+
ChainSelector: 1,
424+
Metadata: testMetadata{Field: "Record3", ChainSelector: 0},
425+
}
426+
)
427+
428+
tests := []struct {
429+
name string
430+
givenState []ChainMetadata
431+
giveChain uint64
432+
expectedResult []ChainMetadata
433+
}{
434+
{
435+
name: "success: returns records with given chain",
436+
givenState: []ChainMetadata{
437+
recordOne,
438+
recordTwo,
439+
recordThree,
440+
},
441+
giveChain: 1,
442+
expectedResult: []ChainMetadata{
443+
recordOne,
444+
recordThree,
445+
},
446+
},
447+
{
448+
name: "success: returns no records with given chain",
449+
givenState: []ChainMetadata{
450+
recordOne,
451+
recordTwo,
452+
recordThree,
453+
},
454+
giveChain: 3,
455+
expectedResult: []ChainMetadata{},
456+
},
457+
}
458+
459+
for _, tt := range tests {
460+
t.Run(tt.name, func(t *testing.T) {
461+
t.Parallel()
462+
463+
filter := ChainMetadataByChainSelector(tt.giveChain)
464+
filteredRecords := filter(tt.givenState)
465+
assert.Equal(t, tt.expectedResult, filteredRecords)
466+
})
467+
}
468+
}

0 commit comments

Comments
 (0)