diff --git a/.changeset/empty-words-tickle.md b/.changeset/empty-words-tickle.md new file mode 100644 index 00000000..aaadaf8f --- /dev/null +++ b/.changeset/empty-words-tickle.md @@ -0,0 +1,13 @@ +--- +"chainlink-deployments-framework": minor +--- + +feat(chain): introduce lazy chain loading + +Feature toggle under CLD_LAZY_BLOCKCHAINS environment variable to enable lazy loading of chains. +Migration guide: + +- Previously: env.BlockChains.EVMChains() +- Preferred: env.Chains().EVMChains() + +By using the newer Chains() method, you can now access newer features such as loading the chains lazily, which is useful for large environments. diff --git a/chain/blockchain.go b/chain/blockchain.go index 4c98acb5..378e63ce 100644 --- a/chain/blockchain.go +++ b/chain/blockchain.go @@ -26,6 +26,10 @@ var _ BlockChain = ton.Chain{} var _ BlockChain = tron.Chain{} var _ BlockChain = canton.Chain{} +// Compile-time checks that both BlockChains and LazyBlockChains implement BlockChainCollection +var _ BlockChainCollection = BlockChains{} +var _ BlockChainCollection = (*LazyBlockChains)(nil) + // BlockChain is an interface that represents a chain. // A chain can be an EVM chain, Solana chain Aptos chain or others. type BlockChain interface { @@ -37,6 +41,22 @@ type BlockChain interface { Family() string } +// BlockChainCollection defines the common interface for accessing blockchain instances. +// Both BlockChains and LazyBlockChains implement this interface. +type BlockChainCollection interface { + GetBySelector(selector uint64) (BlockChain, error) + Exists(selector uint64) bool + ExistsN(selectors ...uint64) bool + All() iter.Seq2[uint64, BlockChain] + EVMChains() map[uint64]evm.Chain + SolanaChains() map[uint64]solana.Chain + AptosChains() map[uint64]aptos.Chain + SuiChains() map[uint64]sui.Chain + TonChains() map[uint64]ton.Chain + TronChains() map[uint64]tron.Chain + ListChainSelectors(options ...ChainSelectorsOption) []uint64 +} + // BlockChains represents a collection of chains. // It provides querying capabilities for different types of chains. type BlockChains struct { diff --git a/chain/lazy_blockchains.go b/chain/lazy_blockchains.go new file mode 100644 index 00000000..0f563cb8 --- /dev/null +++ b/chain/lazy_blockchains.go @@ -0,0 +1,412 @@ +package chain + +import ( + "context" + "errors" + "fmt" + "iter" + "maps" + "slices" + "sync" + + chainsel "github.com/smartcontractkit/chain-selectors" + + "github.com/smartcontractkit/chainlink-deployments-framework/chain/aptos" + "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm" + "github.com/smartcontractkit/chainlink-deployments-framework/chain/solana" + "github.com/smartcontractkit/chainlink-deployments-framework/chain/sui" + "github.com/smartcontractkit/chainlink-deployments-framework/chain/ton" + "github.com/smartcontractkit/chainlink-deployments-framework/chain/tron" + "github.com/smartcontractkit/chainlink-deployments-framework/pkg/logger" +) + +// ChainLoader is an interface for loading a blockchain instance lazily. +type ChainLoader interface { + Load(ctx context.Context, selector uint64) (BlockChain, error) +} + +// LazyBlockChains is a thread-safe wrapper around BlockChains that loads chains on-demand. +// It maintains a cache of loaded chains and uses ChainLoaders to initialize chains when first accessed. +type LazyBlockChains struct { + mu sync.RWMutex + loadedChains map[uint64]BlockChain + loaders map[string]ChainLoader // keyed by chain family + supportedSelectors map[uint64]string // maps selector to chain family + ctx context.Context //nolint:containedctx // Context is needed for lazy loading operations + lggr logger.Logger +} + +// NewLazyBlockChains creates a new LazyBlockChains instance. +// supportedSelectors maps chain selectors to their family (e.g., "evm", "solana", "aptos"). +// loaders provides the ChainLoader for each family. +// +// Chains are loaded on-demand when first accessed. If a chain fails to load during access +// (via GetBySelector, EVMChains, SolanaChains, etc.), the error is logged using lggr and +// the failing chain is skipped. This ensures graceful degradation - successfully loaded +// chains remain accessible while failures are visible in logs. +func NewLazyBlockChains( + ctx context.Context, + supportedSelectors map[uint64]string, + loaders map[string]ChainLoader, + lggr logger.Logger, +) *LazyBlockChains { + return &LazyBlockChains{ + loadedChains: make(map[uint64]BlockChain), + loaders: loaders, + supportedSelectors: supportedSelectors, + ctx: ctx, + lggr: lggr, + } +} + +// GetBySelector returns a blockchain by its selector, loading it lazily if not already loaded. +func (l *LazyBlockChains) GetBySelector(selector uint64) (BlockChain, error) { + // Fast path: check if already loaded + l.mu.RLock() + if chain, ok := l.loadedChains[selector]; ok { + l.mu.RUnlock() + return chain, nil + } + l.mu.RUnlock() + + // Slow path: need to load the chain + l.mu.Lock() + defer l.mu.Unlock() + + // Double-check after acquiring write lock + if chain, ok := l.loadedChains[selector]; ok { + return chain, nil + } + + // Check if the chain is available + family, ok := l.supportedSelectors[selector] + if !ok { + return nil, ErrBlockChainNotFound + } + + // Get the loader for this family + loader, ok := l.loaders[family] + if !ok { + return nil, ErrBlockChainNotFound + } + + // Load the chain + chain, err := loader.Load(l.ctx, selector) + if err != nil { + return nil, err + } + + // Cache the loaded chain + l.loadedChains[selector] = chain + + return chain, nil +} + +// Exists checks if a chain with the given selector is available (not necessarily loaded). +func (l *LazyBlockChains) Exists(selector uint64) bool { + _, ok := l.supportedSelectors[selector] + return ok +} + +// ExistsN checks if all chains with the given selectors are available. +func (l *LazyBlockChains) ExistsN(selectors ...uint64) bool { + for _, selector := range selectors { + if _, ok := l.supportedSelectors[selector]; !ok { + return false + } + } + + return true +} + +// All returns an iterator over all chains, loading them lazily as they are accessed. +// If a chain fails to load, the error is logged and the chain is skipped. +// +// Note: This method loads chains sequentially during iteration. For faster loading when +// iterating over all chains, consider converting to BlockChains first using ToBlockChains(), +// which loads all chains in parallel, then call All() on the result: +// +// blockChains, err := lazyChains.ToBlockChains() +// if err != nil { +// // handle error +// } +// for selector, chain := range blockChains.All() { +// // chains are already loaded +// } +func (l *LazyBlockChains) All() iter.Seq2[uint64, BlockChain] { + return func(yield func(uint64, BlockChain) bool) { + selectors := slices.Collect(maps.Keys(l.supportedSelectors)) + + // Sort for consistent iteration order + slices.Sort(selectors) + + for _, selector := range selectors { + chain, err := l.GetBySelector(selector) + if err != nil { + l.lggr.Errorw("Failed to load chain during iteration", + "selector", selector, + "error", err, + ) + // Skip chains that fail to load + continue + } + if !yield(selector, chain) { + return + } + } + } +} + +// EVMChains returns a map of all EVM chains, loading them lazily. +// If a chain fails to load, the error is logged and the chain is skipped. +func (l *LazyBlockChains) EVMChains() map[uint64]evm.Chain { + chains, err := l.TryEVMChains() + if err != nil { + l.lggr.Errorw("Failed to load one or more EVM chains", "error", err) + } + + return chains +} + +// SolanaChains returns a map of all Solana chains, loading them lazily. +// If a chain fails to load, the error is logged and the chain is skipped. +func (l *LazyBlockChains) SolanaChains() map[uint64]solana.Chain { + chains, err := l.TrySolanaChains() + if err != nil { + l.lggr.Errorw("Failed to load one or more Solana chains", "error", err) + } + + return chains +} + +// AptosChains returns a map of all Aptos chains, loading them lazily. +// If a chain fails to load, the error is logged and the chain is skipped. +func (l *LazyBlockChains) AptosChains() map[uint64]aptos.Chain { + chains, err := l.TryAptosChains() + if err != nil { + l.lggr.Errorw("Failed to load one or more Aptos chains", "error", err) + } + + return chains +} + +// SuiChains returns a map of all Sui chains, loading them lazily. +// If a chain fails to load, the error is logged and the chain is skipped. +func (l *LazyBlockChains) SuiChains() map[uint64]sui.Chain { + chains, err := l.TrySuiChains() + if err != nil { + l.lggr.Errorw("Failed to load one or more Sui chains", "error", err) + } + + return chains +} + +// TonChains returns a map of all Ton chains, loading them lazily. +// If a chain fails to load, the error is logged and the chain is skipped. +func (l *LazyBlockChains) TonChains() map[uint64]ton.Chain { + chains, err := l.TryTonChains() + if err != nil { + l.lggr.Errorw("Failed to load one or more Ton chains", "error", err) + } + + return chains +} + +// TronChains returns a map of all Tron chains, loading them lazily. +// If a chain fails to load, the error is logged and the chain is skipped. +func (l *LazyBlockChains) TronChains() map[uint64]tron.Chain { + chains, err := l.TryTronChains() + if err != nil { + l.lggr.Errorw("Failed to load one or more Tron chains", "error", err) + } + + return chains +} + +// TryEVMChains attempts to load all EVM chains and returns any errors encountered. +// Unlike EVMChains, this method returns an error if any chain fails to load. +// The error may contain multiple chain load failures wrapped together. +// Successfully loaded chains are still returned in the map. +func (l *LazyBlockChains) TryEVMChains() (map[uint64]evm.Chain, error) { + return tryChains[evm.Chain](l, chainsel.FamilyEVM) +} + +// TrySolanaChains attempts to load all Solana chains and returns any errors encountered. +// Unlike SolanaChains, this method returns an error if any chain fails to load. +// The error may contain multiple chain load failures wrapped together. +// Successfully loaded chains are still returned in the map. +func (l *LazyBlockChains) TrySolanaChains() (map[uint64]solana.Chain, error) { + return tryChains[solana.Chain](l, chainsel.FamilySolana) +} + +// TryAptosChains attempts to load all Aptos chains and returns any errors encountered. +// Unlike AptosChains, this method returns an error if any chain fails to load. +// The error may contain multiple chain load failures wrapped together. +// Successfully loaded chains are still returned in the map. +func (l *LazyBlockChains) TryAptosChains() (map[uint64]aptos.Chain, error) { + return tryChains[aptos.Chain](l, chainsel.FamilyAptos) +} + +// TrySuiChains attempts to load all Sui chains and returns any errors encountered. +// Unlike SuiChains, this method returns an error if any chain fails to load. +// The error may contain multiple chain load failures wrapped together. +// Successfully loaded chains are still returned in the map. +func (l *LazyBlockChains) TrySuiChains() (map[uint64]sui.Chain, error) { + return tryChains[sui.Chain](l, chainsel.FamilySui) +} + +// TryTonChains attempts to load all Ton chains and returns any errors encountered. +// Unlike TonChains, this method returns an error if any chain fails to load. +// The error may contain multiple chain load failures wrapped together. +// Successfully loaded chains are still returned in the map. +func (l *LazyBlockChains) TryTonChains() (map[uint64]ton.Chain, error) { + return tryChains[ton.Chain](l, chainsel.FamilyTon) +} + +// TryTronChains attempts to load all Tron chains and returns any errors encountered. +// Unlike TronChains, this method returns an error if any chain fails to load. +// The error may contain multiple chain load failures wrapped together. +// Successfully loaded chains are still returned in the map. +func (l *LazyBlockChains) TryTronChains() (map[uint64]tron.Chain, error) { + return tryChains[tron.Chain](l, chainsel.FamilyTron) +} + +// ListChainSelectors returns all available chain selectors with optional filtering. +func (l *LazyBlockChains) ListChainSelectors(options ...ChainSelectorsOption) []uint64 { + opts := chainSelectorsOptions{} + for _, option := range options { + option(&opts) + } + + selectors := make([]uint64, 0, len(l.supportedSelectors)) + for selector, family := range l.supportedSelectors { + if opts.excludedChainSels != nil { + if _, excluded := opts.excludedChainSels[selector]; excluded { + continue + } + } + if opts.includedFamilies != nil { + if _, ok := opts.includedFamilies[family]; !ok { + continue + } + } + selectors = append(selectors, selector) + } + + slices.Sort(selectors) + + return selectors +} + +// ToBlockChains converts the LazyBlockChains to a regular BlockChains instance. +// This loads all available chains eagerly. +func (l *LazyBlockChains) ToBlockChains() (BlockChains, error) { + selectors := make([]uint64, 0, len(l.supportedSelectors)) + for selector := range l.supportedSelectors { + selectors = append(selectors, selector) + } + + if len(selectors) == 0 { + return NewBlockChains(make(map[uint64]BlockChain)), nil + } + + // Load chains in parallel using helper + results := l.loadChainsParallel(selectors) + + // Collect results + chains := make(map[uint64]BlockChain) + for res := range results { + if res.err != nil { + return BlockChains{}, fmt.Errorf("failed to load chain %d: %w", res.selector, res.err) + } + chains[res.selector] = res.chain + } + + return NewBlockChains(chains), nil +} + +// tryChains is a generic function that attempts to load all chains of a specific family in parallel. +// It returns a map of successfully loaded chains and an error containing all failures. +// Type parameters: +// - T: the chain type (e.g., evm.Chain, solana.Chain) +// - PT: pointer to the chain type (e.g., *evm.Chain) +func tryChains[T any, PT interface { + *T +}](l *LazyBlockChains, family string) (map[uint64]T, error) { + // Get all selectors for this chain family + selectors := make([]uint64, 0) + for selector, f := range l.supportedSelectors { + if f == family { + selectors = append(selectors, selector) + } + } + + if len(selectors) == 0 { + return make(map[uint64]T), nil + } + + // Load chains in parallel using helper + results := l.loadChainsParallel(selectors) + + // Collect results + chains := make(map[uint64]T) + var errs []error + + for res := range results { + if res.err != nil { + errs = append(errs, fmt.Errorf("failed to load %s chain %d: %w", family, res.selector, res.err)) + continue + } + + // Type assertion to convert BlockChain to the specific chain type + switch c := res.chain.(type) { + case T: + chains[res.selector] = c + case PT: + if c != nil { + chains[res.selector] = *c + } + } + } + + if len(errs) > 0 { + return chains, errors.Join(errs...) + } + + return chains, nil +} + +// chainLoadResult represents the result of loading a single chain. +type chainLoadResult struct { + selector uint64 + chain BlockChain + err error +} + +// loadChainsParallel loads multiple chains in parallel and returns a channel of results. +// The channel is closed when all chains have been loaded. +func (l *LazyBlockChains) loadChainsParallel(selectors []uint64) <-chan chainLoadResult { + results := make(chan chainLoadResult, len(selectors)) + var wg sync.WaitGroup + + for _, selector := range selectors { + wg.Add(1) + go func(sel uint64) { + defer wg.Done() + chain, err := l.GetBySelector(sel) + results <- chainLoadResult{ + selector: sel, + chain: chain, + err: err, + } + }(selector) + } + + // Close results channel when all goroutines are done + go func() { + wg.Wait() + close(results) + }() + + return results +} diff --git a/chain/lazy_blockchains_test.go b/chain/lazy_blockchains_test.go new file mode 100644 index 00000000..71c52e2f --- /dev/null +++ b/chain/lazy_blockchains_test.go @@ -0,0 +1,1104 @@ +package chain_test + +import ( + "context" + "strconv" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/zap/zapcore" + + chainsel "github.com/smartcontractkit/chain-selectors" + + "github.com/smartcontractkit/chainlink-deployments-framework/chain" + "github.com/smartcontractkit/chainlink-deployments-framework/pkg/logger" +) + +// Mock chain loader for testing lazy loading +type mockChainLoader struct { + loadFunc func(selector uint64) (chain.BlockChain, error) + loadCalls []uint64 +} + +func (m *mockChainLoader) Load(ctx context.Context, selector uint64) (chain.BlockChain, error) { + m.loadCalls = append(m.loadCalls, selector) + return m.loadFunc(selector) +} + +func TestLazyBlockChains_GetBySelector(t *testing.T) { + t.Parallel() + + t.Run("loads chain on first access", func(t *testing.T) { + t.Parallel() + + loader := &mockChainLoader{ + loadFunc: func(selector uint64) (chain.BlockChain, error) { + if selector == evmChain1.Selector { + return evmChain1, nil + } + + return nil, chain.ErrBlockChainNotFound + }, + } + + availableChains := map[uint64]string{ + evmChain1.Selector: chainsel.FamilyEVM, + } + loaders := map[string]chain.ChainLoader{ + chainsel.FamilyEVM: loader, + } + + lazyChains := chain.NewLazyBlockChains(t.Context(), availableChains, loaders, logger.Nop()) + + // First access should load the chain + got, err := lazyChains.GetBySelector(evmChain1.Selector) + require.NoError(t, err) + assert.Equal(t, evmChain1, got) + assert.Len(t, loader.loadCalls, 1, "chain should be loaded once") + + // Second access should use cache + got, err = lazyChains.GetBySelector(evmChain1.Selector) + require.NoError(t, err) + assert.Equal(t, evmChain1, got) + assert.Len(t, loader.loadCalls, 1, "chain should not be loaded again") + }) + + t.Run("returns error for unavailable chain", func(t *testing.T) { + t.Parallel() + + loader := &mockChainLoader{ + loadFunc: func(selector uint64) (chain.BlockChain, error) { + return evmChain1, nil + }, + } + + availableChains := map[uint64]string{ + evmChain1.Selector: chainsel.FamilyEVM, + } + loaders := map[string]chain.ChainLoader{ + chainsel.FamilyEVM: loader, + } + + lazyChains := chain.NewLazyBlockChains(t.Context(), availableChains, loaders, logger.Nop()) + + // Accessing non-existent chain should return error + _, err := lazyChains.GetBySelector(99999999) + require.Error(t, err) + require.ErrorIs(t, err, chain.ErrBlockChainNotFound) + assert.Empty(t, loader.loadCalls, "loader should not be called for unavailable chains") + }) +} + +func TestLazyBlockChains_Exists(t *testing.T) { + t.Parallel() + + loader := &mockChainLoader{ + loadFunc: func(selector uint64) (chain.BlockChain, error) { + return evmChain1, nil + }, + } + + availableChains := map[uint64]string{ + evmChain1.Selector: chainsel.FamilyEVM, + } + loaders := map[string]chain.ChainLoader{ + chainsel.FamilyEVM: loader, + } + + lazyChains := chain.NewLazyBlockChains(t.Context(), availableChains, loaders, logger.Nop()) + + // Should return true for available chain without loading + assert.True(t, lazyChains.Exists(evmChain1.Selector)) + assert.Empty(t, loader.loadCalls, "Exists should not load the chain") + + // Should return false for unavailable chain + assert.False(t, lazyChains.Exists(99999999)) +} + +func TestLazyBlockChains_EVMChains(t *testing.T) { + t.Parallel() + + loader := &mockChainLoader{ + loadFunc: func(selector uint64) (chain.BlockChain, error) { + switch selector { + case evmChain1.Selector: + return evmChain1, nil + case evmChain2.Selector: + return evmChain2, nil + default: + return nil, chain.ErrBlockChainNotFound + } + }, + } + + availableChains := map[uint64]string{ + evmChain1.Selector: chainsel.FamilyEVM, + evmChain2.Selector: chainsel.FamilyEVM, + solanaChain1.Selector: chainsel.FamilySolana, + } + loaders := map[string]chain.ChainLoader{ + chainsel.FamilyEVM: loader, + chainsel.FamilySolana: loader, + } + + lazyChains := chain.NewLazyBlockChains(t.Context(), availableChains, loaders, logger.Nop()) + + // Get EVM chains should load only EVM chains + evmChains := lazyChains.EVMChains() + assert.Len(t, evmChains, 2, "should return 2 EVM chains") + assert.Contains(t, evmChains, evmChain1.Selector) + assert.Contains(t, evmChains, evmChain2.Selector) + + // Loader should be called for EVM chains only + assert.ElementsMatch(t, []uint64{evmChain1.Selector, evmChain2.Selector}, loader.loadCalls) +} + +func TestLazyBlockChains_All(t *testing.T) { + t.Parallel() + + loader := &mockChainLoader{ + loadFunc: func(selector uint64) (chain.BlockChain, error) { + switch selector { + case evmChain1.Selector: + return evmChain1, nil + case solanaChain1.Selector: + return solanaChain1, nil + default: + return nil, chain.ErrBlockChainNotFound + } + }, + } + + availableChains := map[uint64]string{ + evmChain1.Selector: chainsel.FamilyEVM, + solanaChain1.Selector: chainsel.FamilySolana, + } + loaders := map[string]chain.ChainLoader{ + chainsel.FamilyEVM: loader, + chainsel.FamilySolana: loader, + } + + lazyChains := chain.NewLazyBlockChains(t.Context(), availableChains, loaders, logger.Nop()) + + // Iterate through all chains + count := 0 + for selector, c := range lazyChains.All() { + count++ + assert.NotNil(t, c) + assert.True(t, selector == evmChain1.Selector || selector == solanaChain1.Selector) + } + + assert.Equal(t, 2, count, "should iterate over 2 chains") + assert.Len(t, loader.loadCalls, 2, "should load all chains during iteration") +} + +func TestLazyBlockChains_ListChainSelectors(t *testing.T) { + t.Parallel() + + loader := &mockChainLoader{ + loadFunc: func(selector uint64) (chain.BlockChain, error) { + return evmChain1, nil // Return a valid chain instead of nil, nil + }, + } + + availableChains := map[uint64]string{ + evmChain1.Selector: chainsel.FamilyEVM, + evmChain2.Selector: chainsel.FamilyEVM, + solanaChain1.Selector: chainsel.FamilySolana, + } + loaders := map[string]chain.ChainLoader{ + chainsel.FamilyEVM: loader, + chainsel.FamilySolana: loader, + } + + lazyChains := chain.NewLazyBlockChains(t.Context(), availableChains, loaders, logger.Nop()) + + // List all selectors + selectors := lazyChains.ListChainSelectors() + assert.Len(t, selectors, 3, "should list 3 selectors") + assert.Empty(t, loader.loadCalls, "ListChainSelectors should not load chains") + + // Filter by family + evmSelectors := lazyChains.ListChainSelectors(chain.WithFamily(chainsel.FamilyEVM)) + assert.Len(t, evmSelectors, 2, "should list 2 EVM selectors") + assert.ElementsMatch(t, []uint64{evmChain1.Selector, evmChain2.Selector}, evmSelectors) +} + +func TestLazyBlockChains_ToBlockChains(t *testing.T) { + t.Parallel() + + loader := &mockChainLoader{ + loadFunc: func(selector uint64) (chain.BlockChain, error) { + switch selector { + case evmChain1.Selector: + return evmChain1, nil + case solanaChain1.Selector: + return solanaChain1, nil + default: + return nil, chain.ErrBlockChainNotFound + } + }, + } + + availableChains := map[uint64]string{ + evmChain1.Selector: chainsel.FamilyEVM, + solanaChain1.Selector: chainsel.FamilySolana, + } + loaders := map[string]chain.ChainLoader{ + chainsel.FamilyEVM: loader, + chainsel.FamilySolana: loader, + } + + lazyChains := chain.NewLazyBlockChains(t.Context(), availableChains, loaders, logger.Nop()) + + // Convert to regular BlockChains + blockChains, err := lazyChains.ToBlockChains() + require.NoError(t, err) + + // Should load all chains + assert.Len(t, loader.loadCalls, 2, "should load all chains") + + // Verify chains are accessible + got, err := blockChains.GetBySelector(evmChain1.Selector) + require.NoError(t, err) + assert.Equal(t, evmChain1, got) + + got, err = blockChains.GetBySelector(solanaChain1.Selector) + require.NoError(t, err) + assert.Equal(t, solanaChain1, got) +} + +func TestLazyBlockChains_ToBlockChains_WithError(t *testing.T) { + t.Parallel() + + loader := &mockChainLoader{ + loadFunc: func(selector uint64) (chain.BlockChain, error) { + if selector == evmChain1.Selector { + return evmChain1, nil + } + // Simulate load error for other chains + return nil, assert.AnError + }, + } + + availableChains := map[uint64]string{ + evmChain1.Selector: chainsel.FamilyEVM, + solanaChain1.Selector: chainsel.FamilySolana, + } + loaders := map[string]chain.ChainLoader{ + chainsel.FamilyEVM: loader, + chainsel.FamilySolana: loader, + } + + lazyChains := chain.NewLazyBlockChains(t.Context(), availableChains, loaders, logger.Nop()) + + // ToBlockChains should fail if any chain fails to load + _, err := lazyChains.ToBlockChains() + require.Error(t, err) + assert.Contains(t, err.Error(), "failed to load chain") +} + +func TestLazyBlockChains_EVMChains_LoadError(t *testing.T) { + t.Parallel() + + // Create a logger that we can check for error logs + lggr, logs := logger.TestObserved(t, zapcore.DebugLevel) + + loader := &mockChainLoader{ + loadFunc: func(selector uint64) (chain.BlockChain, error) { + if selector == evmChain1.Selector { + return evmChain1, nil + } + // Simulate a load error for evmChain2 + return nil, assert.AnError + }, + } + + availableChains := map[uint64]string{ + evmChain1.Selector: chainsel.FamilyEVM, + evmChain2.Selector: chainsel.FamilyEVM, + } + loaders := map[string]chain.ChainLoader{ + chainsel.FamilyEVM: loader, + } + + lazyChains := chain.NewLazyBlockChains(t.Context(), availableChains, loaders, lggr) + + // Get EVM chains - should get the successful one and skip the failed one + evmChains := lazyChains.EVMChains() + assert.Len(t, evmChains, 1, "should return only successfully loaded chains") + assert.Contains(t, evmChains, evmChain1.Selector) + assert.NotContains(t, evmChains, evmChain2.Selector) + + // Verify error was logged + assert.Equal(t, 1, logs.FilterMessage("Failed to load one or more EVM chains").Len(), "should log error for failed chain") +} + +func TestLazyBlockChains_SolanaChains(t *testing.T) { + t.Parallel() + + loader := &mockChainLoader{ + loadFunc: func(selector uint64) (chain.BlockChain, error) { + switch selector { + case solanaChain1.Selector: + return solanaChain1, nil + case evmChain1.Selector: + return evmChain1, nil + default: + return nil, chain.ErrBlockChainNotFound + } + }, + } + + availableChains := map[uint64]string{ + solanaChain1.Selector: chainsel.FamilySolana, + evmChain1.Selector: chainsel.FamilyEVM, + } + loaders := map[string]chain.ChainLoader{ + chainsel.FamilySolana: loader, + chainsel.FamilyEVM: loader, + } + + lazyChains := chain.NewLazyBlockChains(t.Context(), availableChains, loaders, logger.Nop()) + + // Get Solana chains should load only Solana chains + solanaChains := lazyChains.SolanaChains() + assert.Len(t, solanaChains, 1, "should return 1 Solana chain") + assert.Contains(t, solanaChains, solanaChain1.Selector) + + // Loader should be called for Solana chain only + assert.ElementsMatch(t, []uint64{solanaChain1.Selector}, loader.loadCalls) +} + +func TestLazyBlockChains_SolanaChains_LoadError(t *testing.T) { + t.Parallel() + + lggr, logs := logger.TestObserved(t, zapcore.DebugLevel) + + loader := &mockChainLoader{ + loadFunc: func(selector uint64) (chain.BlockChain, error) { + return nil, assert.AnError + }, + } + + availableChains := map[uint64]string{ + solanaChain1.Selector: chainsel.FamilySolana, + } + loaders := map[string]chain.ChainLoader{ + chainsel.FamilySolana: loader, + } + + lazyChains := chain.NewLazyBlockChains(t.Context(), availableChains, loaders, lggr) + + // Get Solana chains - should return empty map and log error + solanaChains := lazyChains.SolanaChains() + assert.Empty(t, solanaChains, "should return empty map when load fails") + + // Verify error was logged + assert.Equal(t, 1, logs.FilterMessage("Failed to load one or more Solana chains").Len(), "should log error for failed chain") +} + +func TestLazyBlockChains_AptosChains(t *testing.T) { + t.Parallel() + + loader := &mockChainLoader{ + loadFunc: func(selector uint64) (chain.BlockChain, error) { + switch selector { + case aptosChain1.Selector: + return aptosChain1, nil + default: + return nil, chain.ErrBlockChainNotFound + } + }, + } + + availableChains := map[uint64]string{ + aptosChain1.Selector: chainsel.FamilyAptos, + evmChain1.Selector: chainsel.FamilyEVM, + } + loaders := map[string]chain.ChainLoader{ + chainsel.FamilyAptos: loader, + chainsel.FamilyEVM: loader, + } + + lazyChains := chain.NewLazyBlockChains(t.Context(), availableChains, loaders, logger.Nop()) + + // Get Aptos chains should load only Aptos chains + aptosChains := lazyChains.AptosChains() + assert.Len(t, aptosChains, 1, "should return 1 Aptos chain") + assert.Contains(t, aptosChains, aptosChain1.Selector) + + // Loader should be called for Aptos chain only + assert.ElementsMatch(t, []uint64{aptosChain1.Selector}, loader.loadCalls) +} + +func TestLazyBlockChains_AptosChains_LoadError(t *testing.T) { + t.Parallel() + + lggr, logs := logger.TestObserved(t, zapcore.DebugLevel) + + loader := &mockChainLoader{ + loadFunc: func(selector uint64) (chain.BlockChain, error) { + return nil, assert.AnError + }, + } + + availableChains := map[uint64]string{ + aptosChain1.Selector: chainsel.FamilyAptos, + } + loaders := map[string]chain.ChainLoader{ + chainsel.FamilyAptos: loader, + } + + lazyChains := chain.NewLazyBlockChains(t.Context(), availableChains, loaders, lggr) + + // Get Aptos chains - should return empty map and log error + aptosChains := lazyChains.AptosChains() + assert.Empty(t, aptosChains, "should return empty map when load fails") + + // Verify error was logged + assert.Equal(t, 1, logs.FilterMessage("Failed to load one or more Aptos chains").Len(), "should log error for failed chain") +} + +func TestLazyBlockChains_SuiChains(t *testing.T) { + t.Parallel() + + loader := &mockChainLoader{ + loadFunc: func(selector uint64) (chain.BlockChain, error) { + switch selector { + case suiChain1.Selector: + return suiChain1, nil + default: + return nil, chain.ErrBlockChainNotFound + } + }, + } + + availableChains := map[uint64]string{ + suiChain1.Selector: chainsel.FamilySui, + evmChain1.Selector: chainsel.FamilyEVM, + } + loaders := map[string]chain.ChainLoader{ + chainsel.FamilySui: loader, + chainsel.FamilyEVM: loader, + } + + lazyChains := chain.NewLazyBlockChains(t.Context(), availableChains, loaders, logger.Nop()) + + // Get Sui chains should load only Sui chains + suiChains := lazyChains.SuiChains() + assert.Len(t, suiChains, 1, "should return 1 Sui chain") + assert.Contains(t, suiChains, suiChain1.Selector) + + // Loader should be called for Sui chain only + assert.ElementsMatch(t, []uint64{suiChain1.Selector}, loader.loadCalls) +} + +func TestLazyBlockChains_SuiChains_LoadError(t *testing.T) { + t.Parallel() + + lggr, logs := logger.TestObserved(t, zapcore.DebugLevel) + + loader := &mockChainLoader{ + loadFunc: func(selector uint64) (chain.BlockChain, error) { + return nil, assert.AnError + }, + } + + availableChains := map[uint64]string{ + suiChain1.Selector: chainsel.FamilySui, + } + loaders := map[string]chain.ChainLoader{ + chainsel.FamilySui: loader, + } + + lazyChains := chain.NewLazyBlockChains(t.Context(), availableChains, loaders, lggr) + + // Get Sui chains - should return empty map and log error + suiChains := lazyChains.SuiChains() + assert.Empty(t, suiChains, "should return empty map when load fails") + + // Verify error was logged + assert.Equal(t, 1, logs.FilterMessage("Failed to load one or more Sui chains").Len(), "should log error for failed chain") +} + +func TestLazyBlockChains_TonChains(t *testing.T) { + t.Parallel() + + loader := &mockChainLoader{ + loadFunc: func(selector uint64) (chain.BlockChain, error) { + switch selector { + case tonChain1.Selector: + return tonChain1, nil + default: + return nil, chain.ErrBlockChainNotFound + } + }, + } + + availableChains := map[uint64]string{ + tonChain1.Selector: chainsel.FamilyTon, + evmChain1.Selector: chainsel.FamilyEVM, + } + loaders := map[string]chain.ChainLoader{ + chainsel.FamilyTon: loader, + chainsel.FamilyEVM: loader, + } + + lazyChains := chain.NewLazyBlockChains(t.Context(), availableChains, loaders, logger.Nop()) + + // Get Ton chains should load only Ton chains + tonChains := lazyChains.TonChains() + assert.Len(t, tonChains, 1, "should return 1 Ton chain") + assert.Contains(t, tonChains, tonChain1.Selector) + + // Loader should be called for Ton chain only + assert.ElementsMatch(t, []uint64{tonChain1.Selector}, loader.loadCalls) +} + +func TestLazyBlockChains_TonChains_LoadError(t *testing.T) { + t.Parallel() + + lggr, logs := logger.TestObserved(t, zapcore.DebugLevel) + + loader := &mockChainLoader{ + loadFunc: func(selector uint64) (chain.BlockChain, error) { + return nil, assert.AnError + }, + } + + availableChains := map[uint64]string{ + tonChain1.Selector: chainsel.FamilyTon, + } + loaders := map[string]chain.ChainLoader{ + chainsel.FamilyTon: loader, + } + + lazyChains := chain.NewLazyBlockChains(t.Context(), availableChains, loaders, lggr) + + // Get Ton chains - should return empty map and log error + tonChains := lazyChains.TonChains() + assert.Empty(t, tonChains, "should return empty map when load fails") + + // Verify error was logged + assert.Equal(t, 1, logs.FilterMessage("Failed to load one or more Ton chains").Len(), "should log error for failed chain") +} + +func TestLazyBlockChains_TronChains(t *testing.T) { + t.Parallel() + + loader := &mockChainLoader{ + loadFunc: func(selector uint64) (chain.BlockChain, error) { + switch selector { + case tronChain1.Selector: + return tronChain1, nil + default: + return nil, chain.ErrBlockChainNotFound + } + }, + } + + availableChains := map[uint64]string{ + tronChain1.Selector: chainsel.FamilyTron, + evmChain1.Selector: chainsel.FamilyEVM, + } + loaders := map[string]chain.ChainLoader{ + chainsel.FamilyTron: loader, + chainsel.FamilyEVM: loader, + } + + lazyChains := chain.NewLazyBlockChains(t.Context(), availableChains, loaders, logger.Nop()) + + // Get Tron chains should load only Tron chains + tronChains := lazyChains.TronChains() + assert.Len(t, tronChains, 1, "should return 1 Tron chain") + assert.Contains(t, tronChains, tronChain1.Selector) + + // Loader should be called for Tron chain only + assert.ElementsMatch(t, []uint64{tronChain1.Selector}, loader.loadCalls) +} + +func TestLazyBlockChains_TronChains_LoadError(t *testing.T) { + t.Parallel() + + lggr, logs := logger.TestObserved(t, zapcore.DebugLevel) + + loader := &mockChainLoader{ + loadFunc: func(selector uint64) (chain.BlockChain, error) { + return nil, assert.AnError + }, + } + + availableChains := map[uint64]string{ + tronChain1.Selector: chainsel.FamilyTron, + } + loaders := map[string]chain.ChainLoader{ + chainsel.FamilyTron: loader, + } + + lazyChains := chain.NewLazyBlockChains(t.Context(), availableChains, loaders, lggr) + + // Get Tron chains - should return empty map and log error + tronChains := lazyChains.TronChains() + assert.Empty(t, tronChains, "should return empty map when load fails") + + // Verify error was logged + assert.Equal(t, 1, logs.FilterMessage("Failed to load one or more Tron chains").Len(), "should log error for failed chain") +} + +func TestLazyBlockChains_All_LoadError(t *testing.T) { + t.Parallel() + + lggr, logs := logger.TestObserved(t, zapcore.DebugLevel) + + loader := &mockChainLoader{ + loadFunc: func(selector uint64) (chain.BlockChain, error) { + if selector == evmChain1.Selector { + return evmChain1, nil + } + // Fail to load solana chain + return nil, assert.AnError + }, + } + + availableChains := map[uint64]string{ + evmChain1.Selector: chainsel.FamilyEVM, + solanaChain1.Selector: chainsel.FamilySolana, + } + loaders := map[string]chain.ChainLoader{ + chainsel.FamilyEVM: loader, + chainsel.FamilySolana: loader, + } + + lazyChains := chain.NewLazyBlockChains(t.Context(), availableChains, loaders, lggr) + + // Iterate through all chains - should skip the failed one + count := 0 + for selector, c := range lazyChains.All() { + count++ + assert.NotNil(t, c) + assert.Equal(t, evmChain1.Selector, selector) + } + + assert.Equal(t, 1, count, "should iterate over only successfully loaded chains") + + // Verify error was logged + assert.Equal(t, 1, logs.FilterMessage("Failed to load chain during iteration").Len(), "should log error for failed chain") +} + +func TestLazyBlockChains_TryEVMChains_Success(t *testing.T) { + t.Parallel() + + loader := &mockChainLoader{ + loadFunc: func(selector uint64) (chain.BlockChain, error) { + switch selector { + case evmChain1.Selector: + return evmChain1, nil + case evmChain2.Selector: + return evmChain2, nil + default: + return nil, chain.ErrBlockChainNotFound + } + }, + } + + availableChains := map[uint64]string{ + evmChain1.Selector: chainsel.FamilyEVM, + evmChain2.Selector: chainsel.FamilyEVM, + solanaChain1.Selector: chainsel.FamilySolana, + } + loaders := map[string]chain.ChainLoader{ + chainsel.FamilyEVM: loader, + chainsel.FamilySolana: loader, + } + + lazyChains := chain.NewLazyBlockChains(t.Context(), availableChains, loaders, logger.Nop()) + + // Try to get EVM chains - should succeed with no error + evmChains, err := lazyChains.TryEVMChains() + require.NoError(t, err) + assert.Len(t, evmChains, 2, "should return 2 EVM chains") + assert.Contains(t, evmChains, evmChain1.Selector) + assert.Contains(t, evmChains, evmChain2.Selector) +} + +func TestLazyBlockChains_TryEVMChains_PartialFailure(t *testing.T) { + t.Parallel() + + loader := &mockChainLoader{ + loadFunc: func(selector uint64) (chain.BlockChain, error) { + if selector == evmChain1.Selector { + return evmChain1, nil + } + // Fail to load evmChain2 + return nil, assert.AnError + }, + } + + availableChains := map[uint64]string{ + evmChain1.Selector: chainsel.FamilyEVM, + evmChain2.Selector: chainsel.FamilyEVM, + } + loaders := map[string]chain.ChainLoader{ + chainsel.FamilyEVM: loader, + } + + lazyChains := chain.NewLazyBlockChains(t.Context(), availableChains, loaders, logger.Nop()) + + // Try to get EVM chains - should return error but also successful chains + evmChains, err := lazyChains.TryEVMChains() + require.Error(t, err) + assert.Contains(t, err.Error(), "failed to load evm chain") + assert.Contains(t, err.Error(), strconv.FormatUint(evmChain2.Selector, 10)) + + // Should still get the successfully loaded chain + assert.Len(t, evmChains, 1, "should return successfully loaded chains") + assert.Contains(t, evmChains, evmChain1.Selector) +} + +func TestLazyBlockChains_TryEVMChains_AllFail(t *testing.T) { + t.Parallel() + + loader := &mockChainLoader{ + loadFunc: func(selector uint64) (chain.BlockChain, error) { + return nil, assert.AnError + }, + } + + availableChains := map[uint64]string{ + evmChain1.Selector: chainsel.FamilyEVM, + evmChain2.Selector: chainsel.FamilyEVM, + } + loaders := map[string]chain.ChainLoader{ + chainsel.FamilyEVM: loader, + } + + lazyChains := chain.NewLazyBlockChains(t.Context(), availableChains, loaders, logger.Nop()) + + // Try to get EVM chains - should return error with empty map + evmChains, err := lazyChains.TryEVMChains() + require.Error(t, err) + + // Error should contain both chain selectors + assert.Contains(t, err.Error(), strconv.FormatUint(evmChain1.Selector, 10)) + assert.Contains(t, err.Error(), strconv.FormatUint(evmChain2.Selector, 10)) + + assert.Empty(t, evmChains, "should return empty map when all fail") +} + +func TestLazyBlockChains_TrySolanaChains_Success(t *testing.T) { + t.Parallel() + + loader := &mockChainLoader{ + loadFunc: func(selector uint64) (chain.BlockChain, error) { + if selector == solanaChain1.Selector { + return solanaChain1, nil + } + + return nil, chain.ErrBlockChainNotFound + }, + } + + availableChains := map[uint64]string{ + solanaChain1.Selector: chainsel.FamilySolana, + evmChain1.Selector: chainsel.FamilyEVM, + } + loaders := map[string]chain.ChainLoader{ + chainsel.FamilySolana: loader, + chainsel.FamilyEVM: loader, + } + + lazyChains := chain.NewLazyBlockChains(t.Context(), availableChains, loaders, logger.Nop()) + + // Try to get Solana chains - should succeed + solanaChains, err := lazyChains.TrySolanaChains() + require.NoError(t, err) + assert.Len(t, solanaChains, 1) + assert.Contains(t, solanaChains, solanaChain1.Selector) +} + +func TestLazyBlockChains_TrySolanaChains_Failure(t *testing.T) { + t.Parallel() + + loader := &mockChainLoader{ + loadFunc: func(selector uint64) (chain.BlockChain, error) { + return nil, assert.AnError + }, + } + + availableChains := map[uint64]string{ + solanaChain1.Selector: chainsel.FamilySolana, + } + loaders := map[string]chain.ChainLoader{ + chainsel.FamilySolana: loader, + } + + lazyChains := chain.NewLazyBlockChains(t.Context(), availableChains, loaders, logger.Nop()) + + // Try to get Solana chains - should return error + solanaChains, err := lazyChains.TrySolanaChains() + require.Error(t, err) + assert.Contains(t, err.Error(), "failed to load solana chain") + assert.Empty(t, solanaChains) +} + +func TestLazyBlockChains_TryAptosChains_Success(t *testing.T) { + t.Parallel() + + loader := &mockChainLoader{ + loadFunc: func(selector uint64) (chain.BlockChain, error) { + if selector == aptosChain1.Selector { + return aptosChain1, nil + } + + return nil, chain.ErrBlockChainNotFound + }, + } + + availableChains := map[uint64]string{ + aptosChain1.Selector: chainsel.FamilyAptos, + } + loaders := map[string]chain.ChainLoader{ + chainsel.FamilyAptos: loader, + } + + lazyChains := chain.NewLazyBlockChains(t.Context(), availableChains, loaders, logger.Nop()) + + // Try to get Aptos chains - should succeed + aptosChains, err := lazyChains.TryAptosChains() + require.NoError(t, err) + assert.Len(t, aptosChains, 1) + assert.Contains(t, aptosChains, aptosChain1.Selector) +} + +func TestLazyBlockChains_TryAptosChains_Failure(t *testing.T) { + t.Parallel() + + loader := &mockChainLoader{ + loadFunc: func(selector uint64) (chain.BlockChain, error) { + return nil, assert.AnError + }, + } + + availableChains := map[uint64]string{ + aptosChain1.Selector: chainsel.FamilyAptos, + } + loaders := map[string]chain.ChainLoader{ + chainsel.FamilyAptos: loader, + } + + lazyChains := chain.NewLazyBlockChains(t.Context(), availableChains, loaders, logger.Nop()) + + // Try to get Aptos chains - should return error + aptosChains, err := lazyChains.TryAptosChains() + require.Error(t, err) + assert.Contains(t, err.Error(), "failed to load aptos chain") + assert.Empty(t, aptosChains) +} + +func TestLazyBlockChains_TrySuiChains_Success(t *testing.T) { + t.Parallel() + + loader := &mockChainLoader{ + loadFunc: func(selector uint64) (chain.BlockChain, error) { + if selector == suiChain1.Selector { + return suiChain1, nil + } + + return nil, chain.ErrBlockChainNotFound + }, + } + + availableChains := map[uint64]string{ + suiChain1.Selector: chainsel.FamilySui, + } + loaders := map[string]chain.ChainLoader{ + chainsel.FamilySui: loader, + } + + lazyChains := chain.NewLazyBlockChains(t.Context(), availableChains, loaders, logger.Nop()) + + // Try to get Sui chains - should succeed + suiChains, err := lazyChains.TrySuiChains() + require.NoError(t, err) + assert.Len(t, suiChains, 1) + assert.Contains(t, suiChains, suiChain1.Selector) +} + +func TestLazyBlockChains_TrySuiChains_Failure(t *testing.T) { + t.Parallel() + + loader := &mockChainLoader{ + loadFunc: func(selector uint64) (chain.BlockChain, error) { + return nil, assert.AnError + }, + } + + availableChains := map[uint64]string{ + suiChain1.Selector: chainsel.FamilySui, + } + loaders := map[string]chain.ChainLoader{ + chainsel.FamilySui: loader, + } + + lazyChains := chain.NewLazyBlockChains(t.Context(), availableChains, loaders, logger.Nop()) + + // Try to get Sui chains - should return error + suiChains, err := lazyChains.TrySuiChains() + require.Error(t, err) + assert.Contains(t, err.Error(), "failed to load sui chain") + assert.Empty(t, suiChains) +} + +func TestLazyBlockChains_TryTonChains_Success(t *testing.T) { + t.Parallel() + + loader := &mockChainLoader{ + loadFunc: func(selector uint64) (chain.BlockChain, error) { + if selector == tonChain1.Selector { + return tonChain1, nil + } + + return nil, chain.ErrBlockChainNotFound + }, + } + + availableChains := map[uint64]string{ + tonChain1.Selector: chainsel.FamilyTon, + } + loaders := map[string]chain.ChainLoader{ + chainsel.FamilyTon: loader, + } + + lazyChains := chain.NewLazyBlockChains(t.Context(), availableChains, loaders, logger.Nop()) + + // Try to get Ton chains - should succeed + tonChains, err := lazyChains.TryTonChains() + require.NoError(t, err) + assert.Len(t, tonChains, 1) + assert.Contains(t, tonChains, tonChain1.Selector) +} + +func TestLazyBlockChains_TryTonChains_Failure(t *testing.T) { + t.Parallel() + + loader := &mockChainLoader{ + loadFunc: func(selector uint64) (chain.BlockChain, error) { + return nil, assert.AnError + }, + } + + availableChains := map[uint64]string{ + tonChain1.Selector: chainsel.FamilyTon, + } + loaders := map[string]chain.ChainLoader{ + chainsel.FamilyTon: loader, + } + + lazyChains := chain.NewLazyBlockChains(t.Context(), availableChains, loaders, logger.Nop()) + + // Try to get Ton chains - should return error + tonChains, err := lazyChains.TryTonChains() + require.Error(t, err) + assert.Contains(t, err.Error(), "failed to load ton chain") + assert.Empty(t, tonChains) +} + +func TestLazyBlockChains_TryTronChains_Success(t *testing.T) { + t.Parallel() + + loader := &mockChainLoader{ + loadFunc: func(selector uint64) (chain.BlockChain, error) { + if selector == tronChain1.Selector { + return tronChain1, nil + } + + return nil, chain.ErrBlockChainNotFound + }, + } + + availableChains := map[uint64]string{ + tronChain1.Selector: chainsel.FamilyTron, + } + loaders := map[string]chain.ChainLoader{ + chainsel.FamilyTron: loader, + } + + lazyChains := chain.NewLazyBlockChains(t.Context(), availableChains, loaders, logger.Nop()) + + // Try to get Tron chains - should succeed + tronChains, err := lazyChains.TryTronChains() + require.NoError(t, err) + assert.Len(t, tronChains, 1) + assert.Contains(t, tronChains, tronChain1.Selector) +} + +func TestLazyBlockChains_TryTronChains_Failure(t *testing.T) { + t.Parallel() + + loader := &mockChainLoader{ + loadFunc: func(selector uint64) (chain.BlockChain, error) { + return nil, assert.AnError + }, + } + + availableChains := map[uint64]string{ + tronChain1.Selector: chainsel.FamilyTron, + } + loaders := map[string]chain.ChainLoader{ + chainsel.FamilyTron: loader, + } + + lazyChains := chain.NewLazyBlockChains(t.Context(), availableChains, loaders, logger.Nop()) + + // Try to get Tron chains - should return error + tronChains, err := lazyChains.TryTronChains() + require.Error(t, err) + assert.Contains(t, err.Error(), "failed to load tron chain") + assert.Empty(t, tronChains) +} + +// TestLazyBlockChains_TryEVMChains_WithPointers tests that the generic tryChains +// function correctly handles both value and pointer return types from loaders. +func TestLazyBlockChains_TryEVMChains_WithPointers(t *testing.T) { + t.Parallel() + + // Test with loader that returns pointers + loader := &mockChainLoader{ + loadFunc: func(selector uint64) (chain.BlockChain, error) { + switch selector { + case evmChain1.Selector: + // Return pointer to test the PT case in tryChains type switch + chainCopy := evmChain1 + return &chainCopy, nil + case evmChain2.Selector: + // Return value to test the T case in tryChains type switch + return evmChain2, nil + default: + return nil, chain.ErrBlockChainNotFound + } + }, + } + + availableChains := map[uint64]string{ + evmChain1.Selector: chainsel.FamilyEVM, + evmChain2.Selector: chainsel.FamilyEVM, + } + loaders := map[string]chain.ChainLoader{ + chainsel.FamilyEVM: loader, + } + + lazyChains := chain.NewLazyBlockChains(t.Context(), availableChains, loaders, logger.Nop()) + + // Try to get EVM chains - should handle both pointers and values + evmChains, err := lazyChains.TryEVMChains() + require.NoError(t, err) + assert.Len(t, evmChains, 2, "should return 2 EVM chains regardless of pointer/value") + assert.Contains(t, evmChains, evmChain1.Selector) + assert.Contains(t, evmChains, evmChain2.Selector) + + // Verify the chains are properly dereferenced + assert.Equal(t, evmChain1.Selector, evmChains[evmChain1.Selector].Selector) + assert.Equal(t, evmChain2.Selector, evmChains[evmChain2.Selector].Selector) +} diff --git a/deployment/environment.go b/deployment/environment.go index 8b05e82c..a7814527 100644 --- a/deployment/environment.go +++ b/deployment/environment.go @@ -58,14 +58,58 @@ type Environment struct { OCRSecrets ocr.OCRSecrets // OperationsBundle contains dependencies required by the operations API. OperationsBundle operations.Bundle - // BlockChains is the container of all chains in the environment. + // blockChains is the internal field that holds different blockchain implementations + blockChains chain.BlockChainCollection + + // Use Chains() method instead. This field may be removed in a future major version. Migration: env.BlockChains.EVMChains() → env.Chains().EVMChains() BlockChains chain.BlockChains } // EnvironmentOption is a functional option for configuring an Environment type EnvironmentOption func(*Environment) +// NewEnvironmentWithChains creates a new environment with support for lazy blockchain loading. +func NewEnvironmentWithChains( + name string, + logger logger.Logger, + existingAddrs AddressBook, + dataStore datastore.DataStore, + nodeIDs []string, + offchain offchain.Client, + ctx func() context.Context, + secrets ocr.OCRSecrets, + operationsBundle operations.Bundle, + blockChains chain.BlockChainCollection, + opts ...EnvironmentOption, +) *Environment { + env := &Environment{ + Name: name, + Logger: logger, + ExistingAddresses: existingAddrs, + DataStore: dataStore, + NodeIDs: nodeIDs, + Offchain: offchain, + GetContext: ctx, + OCRSecrets: secrets, + OperationsBundle: operationsBundle, + blockChains: blockChains, + } + + // Backward compatibility: populate deprecated BlockChains field if using eager loading + if bc, ok := blockChains.(chain.BlockChains); ok { + env.BlockChains = bc + } + + // Apply functional options + for _, opt := range opts { + opt(env) + } + + return env +} + // NewEnvironment creates a new environment for CLDF. +// For lazy blockchain loading support, use NewEnvironmentWithChains instead. func NewEnvironment( name string, logger logger.Logger, @@ -89,7 +133,8 @@ func NewEnvironment( OCRSecrets: secrets, // default to memory reporter as that is the only reporter available for now OperationsBundle: operations.NewBundle(ctx, logger, operations.NewMemoryReporter()), - BlockChains: blockChains, + blockChains: blockChains, + BlockChains: blockChains, // Populate deprecated field for backward compatibility } // Apply functional options @@ -100,6 +145,21 @@ func NewEnvironment( return env } +// Chains returns the blockchain collection for this environment. +// Use this method instead of accessing the BlockChains field directly. +// +// Migration guide: +// +// Old: env.BlockChains.EVMChains() (field access) +// New: env.Chains().EVMChains() (method call) +func (e *Environment) Chains() chain.BlockChainCollection { + if e.blockChains != nil { + return e.blockChains + } + // Fallback for backward compatibility with old code that set the field directly + return e.BlockChains +} + // Clone creates a copy of the environment with a new reference to the address book. func (e Environment) Clone() Environment { ab := NewMemoryAddressBook() @@ -114,7 +174,7 @@ func (e Environment) Clone() Environment { } } - return Environment{ + cloned := Environment{ Name: e.Name, Logger: e.Logger, ExistingAddresses: ab, @@ -124,8 +184,20 @@ func (e Environment) Clone() Environment { GetContext: e.GetContext, OCRSecrets: e.OCRSecrets, OperationsBundle: e.OperationsBundle, - BlockChains: e.BlockChains, + blockChains: e.blockChains, } + + // Backward compatibility: populate deprecated BlockChains field if using eager loading + if e.blockChains != nil { + if bc, ok := e.blockChains.(chain.BlockChains); ok { + cloned.BlockChains = bc + } + } else { + // Fallback: clone from the deprecated field + cloned.BlockChains = e.BlockChains + } + + return cloned } // ConfirmIfNoError confirms the transaction if no error occurred. diff --git a/engine/cld/chains/chains.go b/engine/cld/chains/chains.go index ea7d220a..a8e93a3d 100644 --- a/engine/cld/chains/chains.go +++ b/engine/cld/chains/chains.go @@ -160,6 +160,63 @@ func LoadChains( return fchain.NewBlockChainsFromSlice(loadedChains), nil } +// NewLazyBlockChains creates a LazyBlockChains instance that defers chain loading until first access. +// This improves environment initialization performance by avoiding unnecessary chain connections. +// Chains are loaded on-demand when accessed via GetBySelector, EVMChains, SolanaChains, etc. +// All chains defined in the network config are made available for lazy loading. +// +// If a chain fails to load during access, the error is logged and the failing chain is skipped. +// This ensures graceful degradation - successfully loaded chains remain accessible while failures +// are visible in logs. +func NewLazyBlockChains( + ctx context.Context, + lggr logger.Logger, + cfg *config.Config, +) (*fchain.LazyBlockChains, error) { + chainLoaders := newChainLoaders(lggr, cfg.Networks, cfg.Env.Onchain) + + // Get all chain selectors from the network config + allChainSelectors := cfg.Networks.ChainSelectors() + + // Build a map of supported selectors (selector -> family) + supportedSelectors := make(map[uint64]string) + + for _, selector := range allChainSelectors { + // Get the chain family for this selector + chainFamily, err := chainsel.GetSelectorFamily(selector) + if err != nil { + lggr.Warnw("Unable to get chain family for selector", + "selector", selector, "error", err, + ) + + return nil, fmt.Errorf("unable to get chain family for selector %d", selector) + } + + // Check if we have a loader for this chain family + if _, exists := chainLoaders[chainFamily]; !exists { + lggr.Debugw("No chain loader available for chain family, skipping", + "selector", selector, "family", chainFamily, + ) + + continue + } + + supportedSelectors[selector] = chainFamily + } + + lggr.Infow("Created lazy blockchain collection", + "supported_selectors", len(supportedSelectors), + "families", len(chainLoaders), + ) + + fchainLoaders := make(map[string]ChainLoader, len(chainLoaders)) + for family, loader := range chainLoaders { + fchainLoaders[family] = loader + } + + return fchain.NewLazyBlockChains(ctx, supportedSelectors, fchainLoaders, lggr), nil +} + // newChainLoaders returns a map of chain loaders for each supported chain family, based on the provided // network config and secrets. Only chain loaders for which all required secrets are present will be created; // if any required secret is missing for a chain family, its loader is omitted and a warning is logged. @@ -211,6 +268,10 @@ func newChainLoaders( return loaders } +// ChainLoader is an alias for fchain.ChainLoader. +// This alias maintains backward compatibility for code that references chains.ChainLoader. +type ChainLoader = fchain.ChainLoader + var ( _ ChainLoader = &chainLoaderAptos{} _ ChainLoader = &chainLoaderSolana{} @@ -219,11 +280,6 @@ var ( _ ChainLoader = &chainLoaderSui{} ) -// ChainLoader is an interface that defines the methods for loading a chain. -type ChainLoader interface { - Load(ctx context.Context, selector uint64) (fchain.BlockChain, error) -} - // baseChainLoader is a base implementation of the ChainLoader interface. It contains the common // fields for all chain loaders. type baseChainLoader struct { diff --git a/engine/cld/environment/environment.go b/engine/cld/environment/environment.go index c9d8c5fb..4a1bc15c 100644 --- a/engine/cld/environment/environment.go +++ b/engine/cld/environment/environment.go @@ -4,7 +4,9 @@ import ( "context" "errors" "fmt" + "os" + fchain "github.com/smartcontractkit/chainlink-deployments-framework/chain" fdatastore "github.com/smartcontractkit/chainlink-deployments-framework/datastore" fdeployment "github.com/smartcontractkit/chainlink-deployments-framework/deployment" cldcatalog "github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/catalog" @@ -75,17 +77,30 @@ func Load( lggr.Infow("Using file-based datastore") } - // default - loads all chains from the networks config - chainSelectorsToLoad := cfg.Networks.ChainSelectors() + var blockChains fchain.BlockChainCollection + if os.Getenv("CLD_LAZY_BLOCKCHAINS") == "true" { + lggr.Infow("Using lazy blockchains") + // Use lazy loading for chains - they will be initialized on first access + // All chains from the network config are made available, but only loaded when accessed + // ENSURE all downstream code uses the Chains() method instead of the BlockChains field + blockChains, err = chains.NewLazyBlockChains(ctx, lggr, cfg) + if err != nil { + return fdeployment.Environment{}, err + } + } else { + lggr.Infow("Using eager blockchains") + // default - loads all chains from the networks config + chainSelectorsToLoad := cfg.Networks.ChainSelectors() - if loadcfg.chainSelectorsToLoad != nil { - lggr.Infow("Override: loading chains", "chains", loadcfg.chainSelectorsToLoad) - chainSelectorsToLoad = loadcfg.chainSelectorsToLoad - } + if loadcfg.chainSelectorsToLoad != nil { + lggr.Infow("Override: loading chains", "chains", loadcfg.chainSelectorsToLoad) + chainSelectorsToLoad = loadcfg.chainSelectorsToLoad + } - blockChains, err := chains.LoadChains(ctx, lggr, cfg, chainSelectorsToLoad) - if err != nil { - return fdeployment.Environment{}, err + blockChains, err = chains.LoadChains(ctx, lggr, cfg, chainSelectorsToLoad) + if err != nil { + return fdeployment.Environment{}, err + } } nodes, err := envdir.LoadNodes() @@ -130,16 +145,15 @@ func Load( getCtx := func() context.Context { return ctx } - return fdeployment.Environment{ - Name: envKey, - Logger: lggr, - ExistingAddresses: ab, - DataStore: ds, - NodeIDs: nodes.Keys(), - Offchain: jd, - GetContext: getCtx, - OCRSecrets: sharedSecrets, - OperationsBundle: operations.NewBundle(getCtx, lggr, loadcfg.reporter, operations.WithOperationRegistry(loadcfg.operationRegistry)), - BlockChains: blockChains, - }, nil + return *fdeployment.NewEnvironmentWithChains( + envKey, + lggr, + ab, + ds, + nodes.Keys(), + jd, + getCtx, + sharedSecrets, + operations.NewBundle(getCtx, lggr, loadcfg.reporter, operations.WithOperationRegistry(loadcfg.operationRegistry)), + blockChains), nil } diff --git a/engine/cld/environment/environment_test.go b/engine/cld/environment/environment_test.go index 7ef1555c..2c9b4c95 100644 --- a/engine/cld/environment/environment_test.go +++ b/engine/cld/environment/environment_test.go @@ -8,6 +8,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + fchain "github.com/smartcontractkit/chainlink-deployments-framework/chain" fdomain "github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/domain" ) @@ -82,6 +83,40 @@ func Test_Load_NoError(t *testing.T) { require.NoError(t, err) } +func Test_Load_WithLazyBlockchains(t *testing.T) { //nolint:paralleltest // Test sets environment variable + // Set the feature flag to enable lazy loading + t.Setenv("CLD_LAZY_BLOCKCHAINS", "true") + + // Set up domain + domain := setupTest(t, setupTestConfig, setupAddressbook, setupDataStore, setupNodes) + + env, err := Load(t.Context(), domain, "staging", WithoutJD(), OnlyLoadChainsFor([]uint64{})) + require.NoError(t, err) + + // Verify environment was created successfully with lazy blockchains + require.NotNil(t, env.Chains()) + + // Verify we got a LazyBlockChains instance + assert.IsType(t, &fchain.LazyBlockChains{}, env.Chains(), "Expected LazyBlockChains instance") +} + +func Test_Load_WithEagerBlockchains(t *testing.T) { + t.Parallel() + + // Explicitly don't set the feature flag - this tests the default eager loading behavior + // Set up domain + domain := setupTest(t, setupTestConfig, setupAddressbook, setupDataStore, setupNodes) + + env, err := Load(t.Context(), domain, "staging", WithoutJD(), OnlyLoadChainsFor([]uint64{})) + require.NoError(t, err) + + // Verify environment was created successfully with eager blockchains + require.NotNil(t, env.Chains()) + + // Verify we got a BlockChains instance (not LazyBlockChains) + assert.IsType(t, fchain.BlockChains{}, env.Chains(), "Expected BlockChains instance") +} + func setupTest(t *testing.T, setupFnc ...func(t *testing.T, domain fdomain.Domain)) fdomain.Domain { t.Helper() diff --git a/engine/cld/legacy/cli/commands/evm.go b/engine/cld/legacy/cli/commands/evm.go index 2a918a98..87f8232a 100644 --- a/engine/cld/legacy/cli/commands/evm.go +++ b/engine/cld/legacy/cli/commands/evm.go @@ -235,10 +235,10 @@ var ( evmNodesFundExample = cli.Examples(` # Fund all nodes with at least 0.5 ETH on chain 1 in staging (using --eth flag) exemplar evm nodes fund --environment staging --selector 1 --eth 0.5 --1559 - + # Fund all nodes with 100 ETH exemplar evm nodes fund --environment staging --selector 1 --eth 100 - + # Fund all nodes with specific wei amount (using --amount flag) exemplar evm nodes fund --environment staging --selector 1 --amount 10000000000000000000 `) @@ -279,7 +279,7 @@ func (c Commands) newEvmNodesFund(domain domain.Domain) *cobra.Command { if !exists { return fmt.Errorf("chain not found for selector %d", chainselector) } - chain := env.BlockChains.EVMChains()[cs.Selector] + chain := env.Chains().EVMChains()[cs.Selector] var targetAmount *big.Int if ethAmount != "" { diff --git a/engine/cld/legacy/cli/mcmsv2/execute_fork_test.go b/engine/cld/legacy/cli/mcmsv2/execute_fork_test.go index c3feeaf5..2fdeeaaa 100644 --- a/engine/cld/legacy/cli/mcmsv2/execute_fork_test.go +++ b/engine/cld/legacy/cli/mcmsv2/execute_fork_test.go @@ -71,7 +71,7 @@ func Test_executeFork(t *testing.T) { //nolint:paralleltest env.BlockChains = cldfchain.NewBlockChains(map[uint64]cldfchain.BlockChain{ chainsel.GETH_TESTNET.Selector: evmChain, }) - chain := slices.Collect(maps.Values(env.BlockChains.EVMChains()))[0] + chain := slices.Collect(maps.Values(env.Chains().EVMChains()))[0] mcmAddress, timelockAddress, callProxyAddress, env := deployMCMS(t, env) saveChangesetOutputs(t, domain, env, "deploy-mcms") @@ -176,7 +176,7 @@ func deployMCMS(t *testing.T, env cldf.Environment) (string, string, string, cld require.NoError(t, err) signerAddress := crypto.PubkeyToAddress(privateKey.PublicKey) - chain := slices.Collect(maps.Values(env.BlockChains.EVMChains()))[0] + chain := slices.Collect(maps.Values(env.Chains().EVMChains()))[0] mcmAddress, env := deployMcm(t, env, chain, signerAddress) timelockAddress, callProxyAddress, env := deployTimelockAndCallProxy(t, env, chain, []string{mcmAddress}, nil, nil) diff --git a/engine/cld/legacy/cli/mcmsv2/mcms_v2.go b/engine/cld/legacy/cli/mcmsv2/mcms_v2.go index ef527bd9..0cc651af 100644 --- a/engine/cld/legacy/cli/mcmsv2/mcms_v2.go +++ b/engine/cld/legacy/cli/mcmsv2/mcms_v2.go @@ -86,7 +86,7 @@ type cfgv2 struct { proposal mcms.Proposal timelockProposal *mcms.TimelockProposal // nil if not a timelock proposal chainSelector uint64 - blockchains chain.BlockChains + blockchains chain.BlockChainCollection envStr string env cldf.Environment forkedEnv cldfenvironment.ForkedEnvironment diff --git a/engine/cld/legacy/cli/mcmsv2/mcms_v2_test.go b/engine/cld/legacy/cli/mcmsv2/mcms_v2_test.go index ff2beada..f75209a8 100644 --- a/engine/cld/legacy/cli/mcmsv2/mcms_v2_test.go +++ b/engine/cld/legacy/cli/mcmsv2/mcms_v2_test.go @@ -353,7 +353,7 @@ func Test_timelockExecuteOptions(t *testing.T) { require.NoError(t, err) t.Cleanup(func() { _ = os.RemoveAll("domains") }) - chain := slices.Collect(maps.Values(envp.BlockChains.EVMChains()))[0] + chain := slices.Collect(maps.Values(envp.Chains().EVMChains()))[0] timelockAddress, _, env := deployTimelockAndCallProxy(t, *envp, chain, nil, nil, nil) errorContains := func(msg string) func(t *testing.T, opts []mcms.Option, err error) { @@ -400,7 +400,7 @@ func Test_timelockExecuteOptions(t *testing.T) { cfg: &cfgv2{ chainSelector: chain.Selector, env: env, - blockchains: env.BlockChains, + blockchains: env.Chains(), timelockProposal: &mcms.TimelockProposal{ TimelockAddresses: map[types.ChainSelector]string{ types.ChainSelector(chain.Selector): timelockAddress, @@ -417,7 +417,7 @@ func Test_timelockExecuteOptions(t *testing.T) { name: "CallProxy option added when addresses is in AddressBook", cfg: &cfgv2{ chainSelector: chain.Selector, - blockchains: env.BlockChains, + blockchains: env.Chains(), env: func() cldf.Environment { modifiedEnv := env modifiedEnv.DataStore = datastore.NewMemoryDataStore().Seal() @@ -441,7 +441,7 @@ func Test_timelockExecuteOptions(t *testing.T) { cfg: &cfgv2{ chainSelector: chain.Selector, env: env, - blockchains: env.BlockChains, + blockchains: env.Chains(), timelockProposal: &mcms.TimelockProposal{ TimelockAddresses: map[types.ChainSelector]string{ types.ChainSelector(1): timelockAddress, @@ -454,7 +454,7 @@ func Test_timelockExecuteOptions(t *testing.T) { name: "failure: address not found in DataStore or AddressBook", cfg: &cfgv2{ chainSelector: chain.Selector, - blockchains: env.BlockChains, + blockchains: env.Chains(), env: func() cldf.Environment { modifiedEnv := env modifiedEnv.DataStore = datastore.NewMemoryDataStore().Seal() @@ -494,7 +494,7 @@ func Test_setRootCommand(t *testing.T) { require.NoError(t, err) t.Cleanup(func() { _ = os.RemoveAll("domains") }) - chain := slices.Collect(maps.Values(env.BlockChains.EVMChains()))[0] + chain := slices.Collect(maps.Values(env.Chains().EVMChains()))[0] inspector := mcmsevmsdk.NewInspector(chain.Client) privateKey, err := crypto.HexToECDSA("ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80") @@ -516,7 +516,7 @@ func Test_setRootCommand(t *testing.T) { chainSelector: chain.Selector, envStr: env.Name, env: *env, - blockchains: env.BlockChains, + blockchains: env.Chains(), }, setup: func(t *testing.T, cfg *cfgv2) string { t.Helper() @@ -545,7 +545,7 @@ func Test_setRootCommand(t *testing.T) { chainSelector: chain.Selector, envStr: env.Name, env: *env, - blockchains: env.BlockChains, + blockchains: env.Chains(), }, setup: func(t *testing.T, cfg *cfgv2) string { t.Helper() @@ -602,7 +602,7 @@ func Test_executeChainCommand(t *testing.T) { require.NoError(t, err) t.Cleanup(func() { _ = os.RemoveAll("domains") }) - chain := slices.Collect(maps.Values(env.BlockChains.EVMChains()))[0] + chain := slices.Collect(maps.Values(env.Chains().EVMChains()))[0] inspector := mcmsevmsdk.NewInspector(chain.Client) privateKey, err := crypto.HexToECDSA("ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80") @@ -624,7 +624,7 @@ func Test_executeChainCommand(t *testing.T) { chainSelector: chain.Selector, envStr: env.Name, env: *env, - blockchains: env.BlockChains, + blockchains: env.Chains(), }, setup: func(t *testing.T, cfg *cfgv2) string { t.Helper() @@ -654,7 +654,7 @@ func Test_executeChainCommand(t *testing.T) { chainSelector: chain.Selector, envStr: env.Name, env: *env, - blockchains: env.BlockChains, + blockchains: env.Chains(), }, setup: func(t *testing.T, cfg *cfgv2) string { t.Helper() diff --git a/engine/test/environment/environment_test.go b/engine/test/environment/environment_test.go index 0488fae3..135c095a 100644 --- a/engine/test/environment/environment_test.go +++ b/engine/test/environment/environment_test.go @@ -175,7 +175,7 @@ func TestLoader_Load_ChainsOption(t *testing.T) { require.NoError(t, err) require.NotNil(t, env) - blockchains := slices.Collect(maps.Values(maps.Collect(env.BlockChains.All()))) + blockchains := slices.Collect(maps.Values(maps.Collect(env.Chains().All()))) require.ElementsMatch(t, chains, blockchains) } @@ -188,7 +188,7 @@ func TestLoader_Load_ChainOptions(t *testing.T) { //nolint:paralleltest // We ar name string opts []LoadOpt wantBlockChainsLen int - assert func(t *testing.T, BlockChains fchain.BlockChains) + assert func(t *testing.T, BlockChains fchain.BlockChainCollection) }{ { name: "succeeds with no options resulting in no block chains", @@ -199,7 +199,7 @@ func TestLoader_Load_ChainOptions(t *testing.T) { //nolint:paralleltest // We ar name: "EVMSimulated with selectors", opts: []LoadOpt{WithEVMSimulated(t, []uint64{chainselectors.TEST_90000001.Selector})}, wantBlockChainsLen: 1, - assert: func(t *testing.T, BlockChains fchain.BlockChains) { + assert: func(t *testing.T, BlockChains fchain.BlockChainCollection) { t.Helper() require.Len(t, BlockChains.EVMChains(), 1) @@ -209,7 +209,7 @@ func TestLoader_Load_ChainOptions(t *testing.T) { //nolint:paralleltest // We ar name: "EVMSimulatedN", opts: []LoadOpt{WithEVMSimulatedN(t, 1)}, wantBlockChainsLen: 1, - assert: func(t *testing.T, BlockChains fchain.BlockChains) { + assert: func(t *testing.T, BlockChains fchain.BlockChainCollection) { t.Helper() require.Len(t, BlockChains.EVMChains(), 1) @@ -222,7 +222,7 @@ func TestLoader_Load_ChainOptions(t *testing.T) { //nolint:paralleltest // We ar BlockTime: 1 * time.Second, })}, wantBlockChainsLen: 1, - assert: func(t *testing.T, BlockChains fchain.BlockChains) { + assert: func(t *testing.T, BlockChains fchain.BlockChainCollection) { t.Helper() require.Len(t, BlockChains.EVMChains(), 1) @@ -235,7 +235,7 @@ func TestLoader_Load_ChainOptions(t *testing.T) { //nolint:paralleltest // We ar BlockTime: 1 * time.Second, })}, wantBlockChainsLen: 1, - assert: func(t *testing.T, BlockChains fchain.BlockChains) { + assert: func(t *testing.T, BlockChains fchain.BlockChainCollection) { t.Helper() require.Len(t, BlockChains.EVMChains(), 1) @@ -252,7 +252,7 @@ func TestLoader_Load_ChainOptions(t *testing.T) { //nolint:paralleltest // We ar WithSuiContainer(t, []uint64{chainselectors.SUI_LOCALNET.Selector}), }, wantBlockChainsLen: 6, - assert: func(t *testing.T, BlockChains fchain.BlockChains) { + assert: func(t *testing.T, BlockChains fchain.BlockChainCollection) { t.Helper() require.Len(t, BlockChains.EVMChains(), 1) // zksync is an EVM chain @@ -274,7 +274,7 @@ func TestLoader_Load_ChainOptions(t *testing.T) { //nolint:paralleltest // We ar WithSuiContainerN(t, 1), }, wantBlockChainsLen: 6, - assert: func(t *testing.T, BlockChains fchain.BlockChains) { + assert: func(t *testing.T, BlockChains fchain.BlockChainCollection) { t.Helper() require.Len(t, BlockChains.EVMChains(), 1) // zksync is an EVM chain @@ -293,7 +293,7 @@ func TestLoader_Load_ChainOptions(t *testing.T) { //nolint:paralleltest // We ar }), }, wantBlockChainsLen: 1, - assert: func(t *testing.T, BlockChains fchain.BlockChains) { + assert: func(t *testing.T, BlockChains fchain.BlockChainCollection) { t.Helper() require.Len(t, BlockChains.TonChains(), 1) }, @@ -306,7 +306,7 @@ func TestLoader_Load_ChainOptions(t *testing.T) { //nolint:paralleltest // We ar }), }, wantBlockChainsLen: 1, - assert: func(t *testing.T, BlockChains fchain.BlockChains) { + assert: func(t *testing.T, BlockChains fchain.BlockChainCollection) { t.Helper() require.Len(t, BlockChains.TonChains(), 1) }, @@ -319,10 +319,10 @@ func TestLoader_Load_ChainOptions(t *testing.T) { //nolint:paralleltest // We ar env, err := loader.Load(t.Context(), tt.opts...) require.NoError(t, err) require.NotNil(t, env) - require.Len(t, maps.Collect(env.BlockChains.All()), tt.wantBlockChainsLen) + require.Len(t, maps.Collect(env.Chains().All()), tt.wantBlockChainsLen) if tt.assert != nil { - tt.assert(t, env.BlockChains) + tt.assert(t, env.Chains()) } }) } diff --git a/engine/test/internal/mcmsutils/executor.go b/engine/test/internal/mcmsutils/executor.go index 9a195786..edd5ffc4 100644 --- a/engine/test/internal/mcmsutils/executor.go +++ b/engine/test/internal/mcmsutils/executor.go @@ -340,7 +340,7 @@ func getBlockchainsForProposal( continue } - b, err := env.BlockChains.GetBySelector(uint64(selector)) + b, err := env.Chains().GetBySelector(uint64(selector)) if err != nil { return nil, fmt.Errorf( "blockchain not found for chain selector %d: ensure the chain is configured in the provided environment: %w", @@ -368,7 +368,7 @@ func getBlockchainsForTimelockProposal( continue } - b, err := env.BlockChains.GetBySelector(uint64(selector)) + b, err := env.Chains().GetBySelector(uint64(selector)) if err != nil { return nil, fmt.Errorf( "blockchain not found for chain selector %d: ensure the chain is configured in the provided environment: %w", diff --git a/experimental/analyzer/evm_analyzer.go b/experimental/analyzer/evm_analyzer.go index a434b22f..9a6b29ea 100644 --- a/experimental/analyzer/evm_analyzer.go +++ b/experimental/analyzer/evm_analyzer.go @@ -187,7 +187,7 @@ func tryEIP1967ProxyFallback( decoder *EVMTxCallDecoder, ) (*DecodedCall, *abi.ABI, string, error) { // Lazily get EVM chain from environment (only when fallback is needed) - evmChains := env.BlockChains.EVMChains() + evmChains := env.Chains().EVMChains() evmChain, exists := evmChains[chainSelector] if !exists { return nil, nil, "", fmt.Errorf("EVM chain not available for selector %d", chainSelector) diff --git a/experimental/analyzer/evm_analyzer_test.go b/experimental/analyzer/evm_analyzer_test.go index 1be8beb8..7dbc7251 100644 --- a/experimental/analyzer/evm_analyzer_test.go +++ b/experimental/analyzer/evm_analyzer_test.go @@ -828,7 +828,7 @@ func TestQueryEIP1967ImplementationSlot(t *testing.T) { t.Helper() env, err := testenv.New(t.Context(), testenv.WithEVMSimulated(t, []uint64{chainSelector})) require.NoError(t, err) - evmChains := env.BlockChains.EVMChains() + evmChains := env.Chains().EVMChains() return evmChains[chainSelector] }, @@ -889,22 +889,24 @@ func TestTryEIP1967ProxyFallback(t *testing.T) { t.Helper() return &DefaultProposalContext{ - AddressesByChain: deployment.AddressesByChain{ - chainSelector: { - proxyAddress: deployment.MustTypeAndVersionFromString("TransparentUpgradeableProxy 1.0.0"), - }, - }, - evmRegistry: &mockEVMRegistry{ - abis: map[string]string{ - "TransparentUpgradeableProxy 1.0.0": proxyABI, - }, - addressesByChain: deployment.AddressesByChain{ + AddressesByChain: deployment.AddressesByChain{ chainSelector: { proxyAddress: deployment.MustTypeAndVersionFromString("TransparentUpgradeableProxy 1.0.0"), }, }, - }, - }, deployment.Environment{} + evmRegistry: &mockEVMRegistry{ + abis: map[string]string{ + "TransparentUpgradeableProxy 1.0.0": proxyABI, + }, + addressesByChain: deployment.AddressesByChain{ + chainSelector: { + proxyAddress: deployment.MustTypeAndVersionFromString("TransparentUpgradeableProxy 1.0.0"), + }, + }, + }, + }, deployment.Environment{ + BlockChains: chain.NewBlockChainsFromSlice([]chain.BlockChain{}), // empty blockchains + } }, chainSelector: chainSelector, proxyAddress: proxyAddress, @@ -956,14 +958,14 @@ func TestTryEIP1967ProxyFallback(t *testing.T) { require.NoError(t, err) // Set storage to return implAddress (via mock wrapper), but don't add it to address book - evmChains := testEnv.BlockChains.EVMChains() + evmChains := testEnv.Chains().EVMChains() evmChain := evmChains[chainSelector] mockChain, err := setEIP1967StorageOnSimulatedChain(t, evmChain, common.HexToAddress(proxyAddress), implAddress) require.NoError(t, err) evmChains[chainSelector] = mockChain // Rebuild BlockChains with all chains (including the mocked one) allChains := make([]chain.BlockChain, 0) - for _, c := range testEnv.BlockChains.All() { + for _, c := range testEnv.Chains().All() { if c.ChainSelector() == chainSelector { allChains = append(allChains, mockChain) } else { @@ -1011,14 +1013,14 @@ func TestTryEIP1967ProxyFallback(t *testing.T) { require.NoError(t, err) // Set storage to return implAddress - evmChains := testEnv.BlockChains.EVMChains() + evmChains := testEnv.Chains().EVMChains() evmChain := evmChains[chainSelector] mockChain, err := setEIP1967StorageOnSimulatedChain(t, evmChain, common.HexToAddress(proxyAddress), implAddress) require.NoError(t, err) evmChains[chainSelector] = mockChain // Rebuild BlockChains with all chains (including the mocked one) allChains := make([]chain.BlockChain, 0) - for _, c := range testEnv.BlockChains.All() { + for _, c := range testEnv.Chains().All() { if c.ChainSelector() == chainSelector { allChains = append(allChains, mockChain) } else { @@ -1065,7 +1067,9 @@ func TestTryEIP1967ProxyFallback(t *testing.T) { setupCtx: func(t *testing.T) (ProposalContext, deployment.Environment) { t.Helper() - return mockProposalContext(t), deployment.Environment{} + return mockProposalContext(t), deployment.Environment{ + BlockChains: chain.NewBlockChainsFromSlice([]chain.BlockChain{}), // empty blockchains + } }, chainSelector: chainSelector, proxyAddress: proxyAddress, @@ -1082,14 +1086,14 @@ func TestTryEIP1967ProxyFallback(t *testing.T) { require.NoError(t, err) // Set storage to return implAddress - evmChains := testEnv.BlockChains.EVMChains() + evmChains := testEnv.Chains().EVMChains() evmChain := evmChains[chainSelector] mockChain, err := setEIP1967StorageOnSimulatedChain(t, evmChain, common.HexToAddress(proxyAddress), implAddress) require.NoError(t, err) evmChains[chainSelector] = mockChain // Rebuild BlockChains with all chains (including the mocked one) allChains := make([]chain.BlockChain, 0) - for _, c := range testEnv.BlockChains.All() { + for _, c := range testEnv.Chains().All() { if c.ChainSelector() == chainSelector { allChains = append(allChains, mockChain) } else { @@ -1149,14 +1153,14 @@ func TestTryEIP1967ProxyFallback(t *testing.T) { require.NoError(t, err) // Set storage to return implAddress - evmChains := testEnv.BlockChains.EVMChains() + evmChains := testEnv.Chains().EVMChains() evmChain := evmChains[chainSelector] mockChain, err := setEIP1967StorageOnSimulatedChain(t, evmChain, common.HexToAddress(proxyAddress), implAddress) require.NoError(t, err) evmChains[chainSelector] = mockChain // Rebuild BlockChains with all chains (including the mocked one) allChains := make([]chain.BlockChain, 0) - for _, c := range testEnv.BlockChains.All() { + for _, c := range testEnv.Chains().All() { if c.ChainSelector() == chainSelector { allChains = append(allChains, mockChain) } else { @@ -1370,14 +1374,14 @@ func TestAnalyzeEVMTransaction_EIP1967ProxyFallback(t *testing.T) { require.NoError(t, err) // Set storage to return implAddress - evmChains := testEnv.BlockChains.EVMChains() + evmChains := testEnv.Chains().EVMChains() evmChain := evmChains[chainSelector] mockChain, err := setEIP1967StorageOnSimulatedChain(t, evmChain, common.HexToAddress(proxyAddress), implAddress) require.NoError(t, err) evmChains[chainSelector] = mockChain // Rebuild BlockChains with all chains (including the mocked one) allChains := make([]chain.BlockChain, 0) - for _, c := range testEnv.BlockChains.All() { + for _, c := range testEnv.Chains().All() { if c.ChainSelector() == chainSelector { allChains = append(allChains, mockChain) } else { @@ -1473,14 +1477,14 @@ func TestAnalyzeEVMTransaction_EIP1967ProxyFallback(t *testing.T) { require.NoError(t, err) // Set storage to return implAddress - evmChains := testEnv.BlockChains.EVMChains() + evmChains := testEnv.Chains().EVMChains() evmChain := evmChains[chainSelector] mockChain, err := setEIP1967StorageOnSimulatedChain(t, evmChain, common.HexToAddress(proxyAddress), implAddress) require.NoError(t, err) evmChains[chainSelector] = mockChain // Rebuild BlockChains with all chains (including the mocked one) allChains := make([]chain.BlockChain, 0) - for _, c := range testEnv.BlockChains.All() { + for _, c := range testEnv.Chains().All() { if c.ChainSelector() == chainSelector { allChains = append(allChains, mockChain) } else { @@ -1523,22 +1527,24 @@ func TestAnalyzeEVMTransaction_EIP1967ProxyFallback(t *testing.T) { t.Helper() return &DefaultProposalContext{ - AddressesByChain: deployment.AddressesByChain{ - chainSelector: { - proxyAddress: deployment.MustTypeAndVersionFromString("TransparentUpgradeableProxy 1.0.0"), - }, - }, - evmRegistry: &mockEVMRegistry{ - abis: map[string]string{ - "TransparentUpgradeableProxy 1.0.0": proxyABI, - }, - addressesByChain: deployment.AddressesByChain{ + AddressesByChain: deployment.AddressesByChain{ chainSelector: { proxyAddress: deployment.MustTypeAndVersionFromString("TransparentUpgradeableProxy 1.0.0"), }, }, - }, - }, deployment.Environment{} + evmRegistry: &mockEVMRegistry{ + abis: map[string]string{ + "TransparentUpgradeableProxy 1.0.0": proxyABI, + }, + addressesByChain: deployment.AddressesByChain{ + chainSelector: { + proxyAddress: deployment.MustTypeAndVersionFromString("TransparentUpgradeableProxy 1.0.0"), + }, + }, + }, + }, deployment.Environment{ + BlockChains: chain.NewBlockChainsFromSlice([]chain.BlockChain{}), // empty blockchains + } }, expectedError: true, errorContains: "error analyzing operation", // Original error, not chain error