@@ -24,7 +24,6 @@ import (
2424 "encoding/hex"
2525 "fmt"
2626 "path/filepath"
27- "strings"
2827
2928 "github.com/psiemens/sconfig"
3029
@@ -130,24 +129,25 @@ func (f *DependencyFlags) AddToCommand(cmd *cobra.Command) {
130129}
131130
132131type DependencyInstaller struct {
133- Gateways map [string ]gateway.Gateway
134- Logger output.Logger
135- State * flowkit.State
136- SaveState bool
137- TargetDir string
138- SkipDeployments bool
139- SkipAlias bool
140- SkipUpdatePrompts bool
141- Update bool
142- DeploymentAccount string
143- Name string
144- logs categorizedLogs
145- dependencies map [string ]config.Dependency
146- accountAliases map [string ]map [string ]flowsdk.Address // network -> account -> alias
147- installCount int // Track number of dependencies installed
148- pendingPrompts []pendingPrompt // Dependencies that need prompts after tree display
149- prompter Prompter // Optional: for testing. If nil, uses real prompts
150- blockHeightCache map [string ]uint64 // Cache of latest block heights per network for consistent pinning
132+ Gateways map [string ]gateway.Gateway
133+ Logger output.Logger
134+ State * flowkit.State
135+ SaveState bool
136+ TargetDir string
137+ SkipDeployments bool
138+ SkipAlias bool
139+ SkipUpdatePrompts bool
140+ Update bool
141+ DeploymentAccount string
142+ Name string
143+ logs categorizedLogs
144+ dependencies map [string ]config.Dependency
145+ accountAliases map [string ]map [string ]flowsdk.Address // network -> account -> alias
146+ installCount int // Track number of dependencies installed
147+ pendingPrompts []pendingPrompt // Dependencies that need prompts after tree display
148+ prompter Prompter // Optional: for testing. If nil, uses real prompts
149+ blockHeightCache map [string ]uint64 // Cache of latest block heights per network for consistent pinning
150+ minQueryableHeightCache map [string ]uint64 // Cache of minimum queryable block heights per network (from CompatibleRange)
151151}
152152
153153type Prompter interface {
@@ -189,23 +189,24 @@ func NewDependencyInstaller(logger output.Logger, state *flowkit.State, saveStat
189189 }
190190
191191 return & DependencyInstaller {
192- Gateways : gateways ,
193- Logger : logger ,
194- State : state ,
195- SaveState : saveState ,
196- TargetDir : targetDir ,
197- SkipDeployments : flags .skipDeployments ,
198- SkipAlias : flags .skipAlias ,
199- SkipUpdatePrompts : flags .skipUpdatePrompts ,
200- Update : flags .update ,
201- DeploymentAccount : flags .deploymentAccount ,
202- Name : flags .name ,
203- dependencies : make (map [string ]config.Dependency ),
204- logs : categorizedLogs {},
205- accountAliases : make (map [string ]map [string ]flowsdk.Address ),
206- pendingPrompts : make ([]pendingPrompt , 0 ),
207- prompter : prompter {},
208- blockHeightCache : make (map [string ]uint64 ),
192+ Gateways : gateways ,
193+ Logger : logger ,
194+ State : state ,
195+ SaveState : saveState ,
196+ TargetDir : targetDir ,
197+ SkipDeployments : flags .skipDeployments ,
198+ SkipAlias : flags .skipAlias ,
199+ SkipUpdatePrompts : flags .skipUpdatePrompts ,
200+ Update : flags .update ,
201+ DeploymentAccount : flags .deploymentAccount ,
202+ Name : flags .name ,
203+ dependencies : make (map [string ]config.Dependency ),
204+ logs : categorizedLogs {},
205+ accountAliases : make (map [string ]map [string ]flowsdk.Address ),
206+ pendingPrompts : make ([]pendingPrompt , 0 ),
207+ prompter : prompter {},
208+ blockHeightCache : make (map [string ]uint64 ),
209+ minQueryableHeightCache : make (map [string ]uint64 ),
209210 }, nil
210211}
211212
@@ -461,6 +462,40 @@ func (di *DependencyInstaller) processDependency(dependency config.Dependency) e
461462 return di .processDependencies (dependency )
462463}
463464
465+ func (di * DependencyInstaller ) getMinQueryableBlockHeight (network string ) (uint64 , error ) {
466+ // Check cache first
467+ if height , ok := di .minQueryableHeightCache [network ]; ok {
468+ return height , nil
469+ }
470+
471+ gw , ok := di .Gateways [network ]
472+ if ! ok {
473+ return 0 , fmt .Errorf ("gateway for network %s not found" , network )
474+ }
475+
476+ ctx := context .Background ()
477+ nodeVersionInfo , err := gw .GetNodeVersionInfo (ctx )
478+ if err != nil {
479+ return 0 , fmt .Errorf ("failed to get node version info for %s: %w" , network , err )
480+ }
481+
482+ if nodeVersionInfo == nil {
483+ return 0 , fmt .Errorf ("node version info is nil for %s" , network )
484+ }
485+
486+ // Get the minimum queryable block height from the compatible range
487+ var minHeight uint64
488+ if nodeVersionInfo .CompatibleRange != nil {
489+ minHeight = nodeVersionInfo .CompatibleRange .StartHeight
490+ }
491+
492+ // Cache the result (only if cache map is initialized)
493+ if di .minQueryableHeightCache != nil {
494+ di .minQueryableHeightCache [network ] = minHeight
495+ }
496+ return minHeight , nil
497+ }
498+
464499// getLatestBlockHeight returns the current block height for a given network.
465500// Results are cached per network to ensure all dependencies in a single install
466501// operation get pinned to the same block height for consistency.
@@ -609,33 +644,29 @@ func (di *DependencyInstaller) fetchDependenciesWithDepth(dependency config.Depe
609644 } else {
610645 // Use pinned block height for frozen dependencies
611646 blockHeight = existingDependency .BlockHeight
612- }
613647
614- accountContracts , err := di .getContractsAtBlockHeight (networkName , address , blockHeight )
615- if err != nil {
616- // If we get a spork-related error (block height too old), fall back to latest
617- // This happens when flow.json has old block heights from before the current spork
618- // We'll check the hash later - if it matches, we just update metadata; if not, normal update flow applies
619- if strings .Contains (err .Error (), "spork root block height" ) || strings .Contains (err .Error (), "key not found" ) {
620- di .Logger .Info (fmt .Sprintf (" %s Block height %d is from before current spork, fetching latest version" , util .PrintEmoji ("⚠️" ), blockHeight ))
648+ // Proactively check if this block height is within the node's compatible range
649+ minQueryableHeight , err := di .getMinQueryableBlockHeight (networkName )
650+ if err != nil {
651+ return fmt .Errorf ("failed to check compatible block height range: %w" , err )
652+ }
653+
654+ if blockHeight < minQueryableHeight {
655+ // Block height is before the minimum queryable height, need to use latest
621656 hadSporkRecovery = true
622- // Get the current block height (will be cached from above for new deps)
623657 latestHeight , err := di .getLatestBlockHeight (networkName )
624658 if err != nil {
625659 return fmt .Errorf ("failed to get latest block height: %w" , err )
626660 }
627- // Fetch at that specific block height
628- accountContracts , err = di .getContractsAtBlockHeight (networkName , address , latestHeight )
629- if err != nil {
630- return fmt .Errorf ("error fetching contracts: %w" , err )
631- }
632- // Update blockHeight so it's used consistently for this dependency
633661 blockHeight = latestHeight
634- } else {
635- return fmt .Errorf ("error fetching contracts: %w" , err )
636662 }
637663 }
638664
665+ accountContracts , err := di .getContractsAtBlockHeight (networkName , address , blockHeight )
666+ if err != nil {
667+ return fmt .Errorf ("error fetching contracts: %w" , err )
668+ }
669+
639670 contract , ok := accountContracts [contractName ]
640671 if ! ok {
641672 return fmt .Errorf ("contract %s not found at address %s" , contractName , address .String ())
0 commit comments