Skip to content
Open
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
5 changes: 5 additions & 0 deletions datastore/memory_datastore.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ func (s *MemoryDataStore) EnvMetadata() MutableEnvMetadataStore {
return s.EnvMetadataStore
}

// WriteMetadata adds address refs and upserts contract and chain metadata and sets env metadata.
func (s *MemoryDataStore) WriteMetadata(bundle MetadataBundle) error {
return WriteMetadataToDatastore(s, bundle)
}

// Merge merges the given mutable data store into the current MemoryDataStore.
func (s *MemoryDataStore) Merge(other DataStore) error {
// Fetch address ref records from the other data store
Expand Down
44 changes: 44 additions & 0 deletions datastore/write_metadata.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package datastore

import "fmt"

// MetadataBundle is address refs plus contract, chain, and env metadata.
type MetadataBundle struct {
// Addresses are the contract addresses that were deployed.
Addresses []AddressRef
// Contracts defines any metadata pertaining to contracts that were deployed.
Contracts []ContractMetadata
// Chain defines any metadata pertaining to the chain that was operated against.
Chain *ChainMetadata
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I get why Chain isn't a slice here, but as we generalize this CCIP-specific logic into a broadly available API, could this become a limitation? It's technically possible to have multiple ChainMetadata records per domain/environment, and other users might need that.

// Env defines any metadata pertaining to the environment that was deployed to.
Env *EnvMetadata
}

// WriteMetadataToDatastore adds address refs and upserts contract and chain metadata and sets env metadata.
func WriteMetadataToDatastore(ds MutableDataStore, bundle MetadataBundle) error {
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of extending the interface of MutableDataStore to include WriteMetadata, i just introduce a helper function here so any variable of type MutableDataStore can use this

for _, ref := range bundle.Addresses {
if err := ds.Addresses().Add(ref); err != nil {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not Upsert? Using Add would fail if, for example, someone called WriteMetadata intending to update an existing AddressRef

return fmt.Errorf("failed to add %s %v at %s on chain %d to datastore: %w",
ref.Type, ref.Version, ref.Address, ref.ChainSelector, err)
}
}
Comment thread
graham-chainlink marked this conversation as resolved.
for _, contract := range bundle.Contracts {
if err := ds.ContractMetadata().Upsert(contract); err != nil {
return fmt.Errorf("failed to upsert contract metadata for %s on chain %d to datastore: %w",
contract.Address, contract.ChainSelector, err)
}
}
if bundle.Chain != nil {
if err := ds.ChainMetadata().Upsert(*bundle.Chain); err != nil {
return fmt.Errorf("failed to upsert chain metadata for chain %d to datastore: %w",
bundle.Chain.ChainSelector, err)
}
}
if bundle.Env != nil {
if err := ds.EnvMetadata().Set(*bundle.Env); err != nil {
return fmt.Errorf("failed to set env metadata to datastore: %w", err)
}
}

return nil
}
163 changes: 163 additions & 0 deletions datastore/write_metadata_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
package datastore

import (
"testing"

"github.com/Masterminds/semver/v3"
"github.com/stretchr/testify/require"
)

func TestWriteMetadataToDatastore(t *testing.T) {
t.Parallel()

contractOne := ContractMetadata{
Address: "0xaaa",
ChainSelector: 1,
Metadata: testMetadata{Field: "contract-one", ChainSelector: 1},
}
contractTwo := ContractMetadata{
Address: "0xbbb",
ChainSelector: 2,
Metadata: testMetadata{Field: "contract-two", ChainSelector: 2},
}
chainMD := ChainMetadata{
ChainSelector: 1,
Metadata: testMetadata{Field: "chain", ChainSelector: 1},
}
envMD := EnvMetadata{
Metadata: testMetadata{Field: "env", ChainSelector: 0},
}

t.Run("via mutable interface", func(t *testing.T) {
t.Parallel()
var ds MutableDataStore = NewMemoryDataStore()
require.NoError(t, WriteMetadataToDatastore(ds, MetadataBundle{}))
})

t.Run("writes addresses and contract metadata", func(t *testing.T) {
t.Parallel()
ds := NewMemoryDataStore()
ref := AddressRef{
Address: "0xabc",
ChainSelector: 1,
Type: "Timelock",
Version: semver.MustParse("1.0.0"),
}
contractMD := ContractMetadata{
Address: "0xabc",
ChainSelector: 1,
Metadata: testMetadata{Field: "contract", ChainSelector: 1},
}

require.NoError(t, WriteMetadataToDatastore(ds, MetadataBundle{
Addresses: []AddressRef{ref},
Contracts: []ContractMetadata{contractMD},
}))

refs, err := ds.Addresses().Fetch()
require.NoError(t, err)
require.Len(t, refs, 1)

contracts, err := ds.ContractMetadata().Fetch()
require.NoError(t, err)
require.Len(t, contracts, 1)

err = WriteMetadataToDatastore(ds, MetadataBundle{Addresses: []AddressRef{ref}})
require.Error(t, err)
require.ErrorIs(t, err, ErrAddressRefExists)
})

t.Run("memory datastore convenience method", func(t *testing.T) {
t.Parallel()
ds := NewMemoryDataStore()
require.NoError(t, ds.WriteMetadata(MetadataBundle{}))
})

t.Run("empty bundle is a no-op", func(t *testing.T) {
t.Parallel()
ds := NewMemoryDataStore()
require.NoError(t, WriteMetadataToDatastore(ds, MetadataBundle{}))

contracts, err := ds.ContractMetadata().Fetch()
require.NoError(t, err)
require.Empty(t, contracts)

chains, err := ds.ChainMetadata().Fetch()
require.NoError(t, err)
require.Empty(t, chains)

_, err = ds.EnvMetadata().Get()
require.ErrorIs(t, err, ErrEnvMetadataNotSet)
})

t.Run("writes contracts chain and env", func(t *testing.T) {
t.Parallel()
ds := NewMemoryDataStore()
require.NoError(t, WriteMetadataToDatastore(ds, MetadataBundle{
Contracts: []ContractMetadata{contractOne, contractTwo},
Chain: &chainMD,
Env: &envMD,
}))

contracts, err := ds.ContractMetadata().Fetch()
require.NoError(t, err)
require.Len(t, contracts, 2)

chains, err := ds.ChainMetadata().Fetch()
require.NoError(t, err)
require.Len(t, chains, 1)
require.Equal(t, uint64(1), chains[0].ChainSelector)

gotEnv, err := ds.EnvMetadata().Get()
require.NoError(t, err)
gotEnvMD, err := As[testMetadata](gotEnv.Metadata)
require.NoError(t, err)
require.Equal(t, "env", gotEnvMD.Field)
})

t.Run("upserts overwrite existing records", func(t *testing.T) {
t.Parallel()
ds := NewMemoryDataStore()
require.NoError(t, WriteMetadataToDatastore(ds, MetadataBundle{
Contracts: []ContractMetadata{contractOne},
Chain: &chainMD,
Env: &envMD,
}))

updatedContract := contractOne
updatedContract.Metadata = testMetadata{Field: "updated", ChainSelector: 1}
updatedChain := ChainMetadata{
ChainSelector: 1,
Metadata: testMetadata{Field: "updated-chain", ChainSelector: 1},
}
updatedEnv := EnvMetadata{
Metadata: testMetadata{Field: "updated-env", ChainSelector: 0},
}

require.NoError(t, WriteMetadataToDatastore(ds, MetadataBundle{
Contracts: []ContractMetadata{updatedContract},
Chain: &updatedChain,
Env: &updatedEnv,
}))

contracts, err := ds.ContractMetadata().Fetch()
require.NoError(t, err)
require.Len(t, contracts, 1)
gotContractMD, err := As[testMetadata](contracts[0].Metadata)
require.NoError(t, err)
require.Equal(t, "updated", gotContractMD.Field)

chains, err := ds.ChainMetadata().Fetch()
require.NoError(t, err)
require.Len(t, chains, 1)
gotChainMD, err := As[testMetadata](chains[0].Metadata)
require.NoError(t, err)
require.Equal(t, "updated-chain", gotChainMD.Field)

gotEnv, err := ds.EnvMetadata().Get()
require.NoError(t, err)
gotEnvMD, err := As[testMetadata](gotEnv.Metadata)
require.NoError(t, err)
require.Equal(t, "updated-env", gotEnvMD.Field)
})
}
Loading